Skip to main content

Overview

This integration guide shows you how to how to update your existing project to:
  1. install the ButterCMS package
  2. instantiate ButterCMS
  3. create components to fetch and display each of the three ButterCMS content types: Pages, Collections, and Blog Posts.
In order for the snippets to work, you’ll need to setup your dashboard content schemas inside of ButterCMS first.

Starter project

Or, you can jump directly to the starter project below, which will allow you to clone, install, run, and deploy a fully working starter project that’s integrated with content already inside of your ButterCMS account.

.NET Starter Project

Hit the ground running with a pre-configured .NET + ButterCMS setup.

Installation

dotnet add package ButterCMS

Initialize the client

// Program.cs
using ButterCMS;

var builder = WebApplication.CreateBuilder(args);

// Register ButterCMS client
builder.Services.AddSingleton<ButterCMSClient>(sp =>
    new ButterCMSClient(builder.Configuration["ButterCMS:ApiToken"]));

builder.Services.AddControllersWithViews();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
// appsettings.json
{
  "ButterCMS": {
    "ApiToken": "your_api_token"
  }
}
For complete SDK documentation including all available methods and configuration options, see the .NET SDK Reference.

Data models

// Models/ButterModels.cs
namespace YourApp.Models;

public class LandingPageFields
{
    public string Headline { get; set; } = "";
    public string Subheadline { get; set; } = "";
    public string HeroImage { get; set; } = "";
    public string Body { get; set; } = "";
    public SEOFields? Seo { get; set; }
    public List<ComponentData>? BodyComponents { get; set; }
}

public class SEOFields
{
    public string Title { get; set; } = "";
    public string Description { get; set; } = "";
    public string OgTitle { get; set; } = "";
    public string OgDescription { get; set; } = "";
    public string OgImage { get; set; } = "";
}

public class BrandCollection
{
    public List<Brand> Brands { get; set; } = new();
}

public class Brand
{
    public string Name { get; set; } = "";
    public string Logo { get; set; } = "";
    public string Description { get; set; } = "";
}

public class ComponentData
{
    public string Type { get; set; } = "";
    public Dictionary<string, object> Fields { get; set; } = new();
}

public class BlogIndexViewModel
{
    public IEnumerable<ButterCMS.Models.Post> Posts { get; set; } = new List<ButterCMS.Models.Post>();
    public ButterCMS.Models.PostsMeta Meta { get; set; } = new();
}

Pages

// Controllers/PageController.cs
using Microsoft.AspNetCore.Mvc;
using ButterCMS;
using YourApp.Models;

public class PageController : Controller
{
    private readonly ButterCMSClient _butter;

    public PageController(ButterCMSClient butter)
    {
        _butter = butter;
    }

    [HttpGet("/")]
    [HttpGet("/page/{slug}")]
    public async Task<IActionResult> Show(string slug = "home")
    {
        try
        {
            var response = await _butter.RetrievePageAsync<LandingPageFields>(
                "landing-page", slug);
            return View("Landing", response.Data);
        }
        catch
        {
            return NotFound();
        }
    }

    [HttpGet("/pages")]
    public async Task<IActionResult> List()
    {
        var response = await _butter.ListPagesAsync<LandingPageFields>("landing-page");
        return View(response.Data);
    }
}
@* Views/Page/Landing.cshtml *@
@model ButterCMS.Models.Page<LandingPageFields>

@{
    ViewData["Title"] = Model.Fields.Seo?.Title ?? Model.Fields.Headline;
    ViewData["Description"] = Model.Fields.Seo?.Description ?? "";
}

<main>
    <h1>@Model.Fields.Headline</h1>
    <p>@Model.Fields.Subheadline</p>

    @if (!string.IsNullOrEmpty(Model.Fields.HeroImage))
    {
        <img src="@Model.Fields.HeroImage" alt="@Model.Fields.Headline" />
    }

    @Html.Raw(Model.Fields.Body)
</main>

Collections

// Controllers/BrandController.cs
using Microsoft.AspNetCore.Mvc;
using ButterCMS;
using YourApp.Models;

public class BrandController : Controller
{
    private readonly ButterCMSClient _butter;

    public BrandController(ButterCMSClient butter)
    {
        _butter = butter;
    }

    [HttpGet("/brands")]
    public async Task<IActionResult> Index()
    {
        var response = await _butter.RetrieveContentFieldsAsync<BrandCollection>(
            new[] { "brands" });
        return View(response.Data.Brands);
    }
}
@* Views/Brand/Index.cshtml *@
@model List<Brand>

<main>
    <h1>Our Brands</h1>
    <ul>
        @foreach (var brand in Model)
        {
            <li>
                <img src="@brand.Logo" alt="@brand.Name" />
                <h2>@brand.Name</h2>
                @Html.Raw(brand.Description)
            </li>
        }
    </ul>
</main>

Dynamic components

Component models

// Models/Components.cs
namespace YourApp.Models;

public class HeroFields
{
    public string Headline { get; set; } = "";
    public string Subheadline { get; set; } = "";
    public string Image { get; set; } = "";
    public string ButtonLabel { get; set; } = "";
    public string ButtonUrl { get; set; } = "";
}

public class FeaturesFields
{
    public string Headline { get; set; } = "";
    public List<FeatureItem> Items { get; set; } = new();
}

public class FeatureItem
{
    public string Title { get; set; } = "";
    public string Description { get; set; } = "";
    public string Icon { get; set; } = "";
}

public class CTAFields
{
    public string Headline { get; set; } = "";
    public string Subheadline { get; set; } = "";
    public string ButtonLabel { get; set; } = "";
    public string ButtonUrl { get; set; } = "";
}

Component renderer

// Services/ComponentRenderer.cs
using System.Text;
using System.Text.Json;
using YourApp.Models;

public static class ComponentRenderer
{
    public static string Render(List<ComponentData> components)
    {
        var sb = new StringBuilder();

        foreach (var component in components)
        {
            sb.Append(component.Type switch
            {
                "hero" => RenderHero(component.Fields),
                "features" => RenderFeatures(component.Fields),
                "cta" => RenderCTA(component.Fields),
                _ => ""
            });
        }

        return sb.ToString();
    }

    private static string RenderHero(Dictionary<string, object> fields)
    {
        var headline = fields.GetValueOrDefault("headline", "")?.ToString() ?? "";
        var subheadline = fields.GetValueOrDefault("subheadline", "")?.ToString() ?? "";
        var image = fields.GetValueOrDefault("image", "")?.ToString() ?? "";
        var buttonLabel = fields.GetValueOrDefault("button_label", "")?.ToString() ?? "";
        var buttonUrl = fields.GetValueOrDefault("button_url", "")?.ToString() ?? "";

        return $@"
            <section class=""hero"">
                <h1>{headline}</h1>
                <p>{subheadline}</p>
                {(string.IsNullOrEmpty(buttonLabel) ? "" : $@"<a href=""{buttonUrl}"" class=""btn"">{buttonLabel}</a>")}
                {(string.IsNullOrEmpty(image) ? "" : $@"<img src=""{image}"" alt=""{headline}"" />")}
            </section>";
    }

    private static string RenderFeatures(Dictionary<string, object> fields)
    {
        var headline = fields.GetValueOrDefault("headline", "")?.ToString() ?? "";
        var items = fields.GetValueOrDefault("items") as JsonElement? ?? default;

        var itemsHtml = new StringBuilder();
        if (items.ValueKind == JsonValueKind.Array)
        {
            foreach (var item in items.EnumerateArray())
            {
                var title = item.GetProperty("title").GetString() ?? "";
                var description = item.GetProperty("description").GetString() ?? "";
                itemsHtml.Append($@"
                    <div class=""feature"">
                        <h3>{title}</h3>
                        <p>{description}</p>
                    </div>");
            }
        }

        return $@"
            <section class=""features"">
                <h2>{headline}</h2>
                <div class=""features-grid"">{itemsHtml}</div>
            </section>";
    }

    private static string RenderCTA(Dictionary<string, object> fields)
    {
        var headline = fields.GetValueOrDefault("headline", "")?.ToString() ?? "";
        var subheadline = fields.GetValueOrDefault("subheadline", "")?.ToString() ?? "";
        var buttonLabel = fields.GetValueOrDefault("button_label", "")?.ToString() ?? "";
        var buttonUrl = fields.GetValueOrDefault("button_url", "")?.ToString() ?? "";

        return $@"
            <section class=""cta"">
                <h2>{headline}</h2>
                <p>{subheadline}</p>
                <a href=""{buttonUrl}"" class=""btn"">{buttonLabel}</a>
            </section>";
    }
}

Example component

@* Views/Shared/Components/Hero.cshtml *@
@model HeroFields

<section class="hero">
    <h1>@Model.Headline</h1>
    <p>@Model.Subheadline</p>

    @if (!string.IsNullOrEmpty(Model.ButtonLabel))
    {
        <a href="@Model.ButtonUrl" class="btn">@Model.ButtonLabel</a>
    }

    @if (!string.IsNullOrEmpty(Model.Image))
    {
        <img src="@Model.Image" alt="@Model.Headline" />
    }
</section>

Using in pages

// Controllers/ComponentPageController.cs
public class ComponentPageController : Controller
{
    private readonly ButterCMSClient _butter;

    public ComponentPageController(ButterCMSClient butter)
    {
        _butter = butter;
    }

    [HttpGet("/landing/{slug}")]
    public async Task<IActionResult> Show(string slug)
    {
        try
        {
            var response = await _butter.RetrievePageAsync<dynamic>(
                "landing-page", slug);

            var page = response.Data;
            var bodyComponents = new List<ComponentData>();

            // Parse components from body field
            if (page.Fields.body != null)
            {
                foreach (var comp in page.Fields.body)
                {
                    bodyComponents.Add(new ComponentData
                    {
                        Type = comp.type.ToString(),
                        Fields = JsonSerializer.Deserialize<Dictionary<string, object>>(
                            comp.fields.ToString())
                    });
                }
            }

            ViewBag.Components = ComponentRenderer.Render(bodyComponents);
            return View("ComponentPage", page);
        }
        catch
        {
            return NotFound();
        }
    }
}
@* Views/ComponentPage/ComponentPage.cshtml *@
@model dynamic

<main>
    @Html.Raw(ViewBag.Components)
</main>

Blog

// Controllers/BlogController.cs
using Microsoft.AspNetCore.Mvc;
using ButterCMS;
using YourApp.Models;

public class BlogController : Controller
{
    private readonly ButterCMSClient _butter;

    public BlogController(ButterCMSClient butter)
    {
        _butter = butter;
    }

    [HttpGet("/blog")]
    public async Task<IActionResult> Index(int page = 1)
    {
        var response = await _butter.ListPostsAsync(page, 10);
        return View(new BlogIndexViewModel
        {
            Posts = response.Data,
            Meta = response.Meta
        });
    }

    [HttpGet("/blog/category/{slug}")]
    public async Task<IActionResult> Category(string slug, int page = 1)
    {
        var response = await _butter.ListPostsAsync(page, 10,
            categorySlug: slug);
        return View("Index", new BlogIndexViewModel
        {
            Posts = response.Data,
            Meta = response.Meta
        });
    }
}
@* Views/Blog/Index.cshtml *@
@model BlogIndexViewModel

<main>
    <h1>Blog</h1>
    <ul>
        @foreach (var post in Model.Posts)
        {
            <li>
                <h2><a asp-action="Show" asp-route-slug="@post.Slug">@post.Title</a></h2>
                <p class="meta">
                    By @post.Author.FirstName @post.Author.LastName
                    on @post.Published?.ToString("MMMM d, yyyy")
                </p>
                <p>@post.Summary</p>
            </li>
        }
    </ul>

    @if (Model.Meta.NextPage.HasValue)
    {
        <a asp-action="Index" asp-route-page="@Model.Meta.NextPage">Next Page</a>
    }
</main>

Preview mode

// Controllers/PageController.cs
[HttpGet("/page/{slug}")]
public async Task<IActionResult> Show(string slug = "home", bool preview = false)
{
    try
    {
        var parameters = new Dictionary<string, string>();
        if (preview)
        {
            parameters["preview"] = "1";
        }

        var response = await _butter.RetrievePageAsync<LandingPageFields>(
            "landing-page", slug, parameters);

        ViewBag.IsPreview = preview;
        return View("Landing", response.Data);
    }
    catch
    {
        return NotFound();
    }
}

Caching

// Services/CachedButterService.cs
using Microsoft.Extensions.Caching.Memory;
using ButterCMS;

public class CachedButterService
{
    private readonly ButterCMSClient _butter;
    private readonly IMemoryCache _cache;
    private readonly TimeSpan _defaultTtl = TimeSpan.FromMinutes(60);

    public CachedButterService(ButterCMSClient butter, IMemoryCache cache)
    {
        _butter = butter;
        _cache = cache;
    }

    public async Task<ButterCMS.Models.Page<T>> GetPageAsync<T>(
        string pageType, string slug, bool preview = false)
    {
        if (preview)
        {
            var previewResponse = await _butter.RetrievePageAsync<T>(
                pageType, slug, new Dictionary<string, string> { { "preview", "1" } });
            return previewResponse.Data;
        }

        var cacheKey = $"page_{pageType}_{slug}";

        return await _cache.GetOrCreateAsync(cacheKey, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _defaultTtl;
            var response = await _butter.RetrievePageAsync<T>(pageType, slug);
            return response.Data;
        }) ?? throw new Exception("Page not found");
    }

    public async Task<IEnumerable<ButterCMS.Models.Post>> GetPostsAsync(int page, int pageSize)
    {
        var cacheKey = $"posts_{page}_{pageSize}";

        return await _cache.GetOrCreateAsync(cacheKey, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _defaultTtl;
            var response = await _butter.ListPostsAsync(page, pageSize);
            return response.Data;
        }) ?? Enumerable.Empty<ButterCMS.Models.Post>();
    }

    public void InvalidateCache(string pattern)
    {
        // For IMemoryCache, you'd need to track keys or use a distributed cache
        // This example shows the concept
    }
}

Webhook cache invalidation

// Controllers/WebhookController.cs
[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
    private readonly CachedButterService _butterService;

    public WebhookController(CachedButterService butterService)
    {
        _butterService = butterService;
    }

    [HttpPost("butter")]
    public IActionResult HandleButterWebhook([FromBody] WebhookPayload payload)
    {
        // Invalidate cache based on webhook event
        if (payload.Webhook?.Event?.Contains("page") == true)
        {
            _butterService.InvalidateCache("page_");
        }

        if (payload.Webhook?.Event?.Contains("post") == true)
        {
            _butterService.InvalidateCache("posts_");
        }

        return Ok(new { status = "ok" });
    }
}

public class WebhookPayload
{
    public WebhookData? Webhook { get; set; }
}

public class WebhookData
{
    public string? Event { get; set; }
}

SEO

@* Views/Shared/_Layout.cshtml *@
<!DOCTYPE html>
<html>
<head>
    <title>@ViewData["Title"] - My Site</title>
    <meta name="description" content="@ViewData["Description"]">

    <!-- Open Graph -->
    <meta property="og:title" content="@(ViewData["OgTitle"] ?? ViewData["Title"])">
    <meta property="og:description" content="@(ViewData["OgDescription"] ?? ViewData["Description"])">
    @if (ViewData["OgImage"] != null)
    {
        <meta property="og:image" content="@ViewData["OgImage"]">
    }

    <!-- Twitter -->
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="@ViewData["Title"]">
    <meta name="twitter:description" content="@ViewData["Description"]">
</head>
<body>
    @RenderBody()
</body>
</html>
@* Views/Page/Landing.cshtml *@
@model ButterCMS.Models.Page<LandingPageFields>

@{
    ViewData["Title"] = Model.Fields.Seo?.Title ?? Model.Fields.Headline;
    ViewData["Description"] = Model.Fields.Seo?.Description ?? "";
    ViewData["OgTitle"] = Model.Fields.Seo?.OgTitle ?? Model.Fields.Seo?.Title ?? Model.Fields.Headline;
    ViewData["OgDescription"] = Model.Fields.Seo?.OgDescription ?? Model.Fields.Seo?.Description ?? "";
    ViewData["OgImage"] = Model.Fields.Seo?.OgImage;
}

<main>
    <!-- Page content -->
</main>

Resources

.NET Starter

Pre-configured starter project

.NET SDK

Complete SDK reference

GitHub Repository

View source code

Webhooks

Set up content webhooks