Build a CMS in C#
Klassd is a code-first, headless CMS for .NET. You define content types — pages, blocks and property types — as plain C# classes; the engine reflects over them to drive a Blazor admin and a JSON delivery API. No schema migrations, no separate modelling UI.
Quickstart
Install the engine plus one storage adapter. While Klassd is in beta the packages are prerelease:
dotnet add package Klassd.Backoffice --prerelease
dotnet add package Klassd.Data.Sqlite --prerelease1. Define content types
Any class deriving from PageBase or BlockBase in your app is discovered automatically:
using Klassd.Core.Abstractions;
[CmsPage(DefaultSlug = "", Icon = "house")]
[AllowedChildren(typeof(ArticlePage))]
public class HomePage : PageBase
{
[Localized] // separate value per locale
public string Title { get; set; } = "";
public string SubTitle { get; set; } = "";
public BlockArea HeroBlocks { get; set; } = new();
}
public class ArticlePage : PageBase
{
public string Title { get; set; } = "";
[CmsField(FieldType = "textarea")]
public string Body { get; set; } = "";
public MediaReference Hero { get; set; } = new();
[AllowedRelations(typeof(ArticlePage))]
public PageReference Related { get; set; } = new();
}2. Wire it up in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddKlassd(builder.Configuration) // discovers your content types
.UseSqlite(builder.Configuration.GetSection("Sqlite")) // or .UseMongoDb / .UsePostgres
.UseInMemoryCache(); // optional read-through cache
var app = builder.Build();
app.UseKlassd(); // auth + antiforgery + seed/init + static assets + /api + Blazor admin
app.Run();Host .csproj: a host with no .razor files of its own must set <RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>, or /admin 404s on blazor.web.js. (If your host already has .razor files the SDK turns this on for you.)
3. Run
Open /admin to author content. Your frontend reads published content from /api/pages.
Content types
- Pages — derive from
PageBase. Live in a tree (parent/child), have a slug, and are delivered at/api/pages. - Blocks — derive from
BlockBase. Reusable content components placed into a page'sBlockAreaproperties; each can be scheduled with a publish window. - Globals — annotate with
[CmsGlobal]. Singletons for site chrome (header, footer) — exactly what this website uses.
Every public property becomes an editable field. The editor for a field is chosen from its CLR type (see below) or an explicit [CmsField(FieldType = "…")].
Built-in property types
Each field maps to a property type by its CLR type, or you can force one with [CmsField(FieldType = "alias")].
| Alias | CLR type | Declare it | Notes |
|---|---|---|---|
text | string | | Default for string. Single-line text input. |
textarea | — (opt-in) | | Multi-line text. Opt in on a string via [CmsField]. |
number | int, long | | Default for int and long. Numeric input. |
checkbox | bool | | Default for bool. Toggle. |
datetime-local | DateTime | | Default for DateTime. Date + time picker. |
blocks | BlockArea | | A named, schedulable area of block instances. Declare one property per area. |
media | MediaReference | | Media picker; stores the media item id. A string with [CmsField(FieldType = "media")] also works. |
relationship | PageReference | | Page picker; stores the target page’s ContentId. Restrict targets with [AllowedRelations]; omit for any page type. |
richtext | — (opt-in) | | Rich text editor (Quill); stores HTML. |
email / url / color | — (opt-in) | | HTML5 email / URL / colour inputs. Opt in on a string via [CmsField]. |
decimal | decimal, double, float | | Fractional number input. |
date / time | DateOnly / TimeOnly | | Date-only / time-only pickers. |
Media options
Media is organised into named sections, each backed by its own blob adapter (FileSystem / S3 / Google Cloud). Configure with .AddMedia(...):
UseFileSystem(path)/UseS3(...)/UseGoogleCloudStorage(...)— pick a backend per section.AllowContentTypes("image/*", "application/pdf")— restrict uploads.ResizeImages(maxEdgePixels)— downscale in the browser before upload.Breakpoints("default", "mobile", "tablet")— focal-point editors the admin offers.
Relationship options
- Declare a
PageReferenceproperty (or[CmsField(FieldType = "relationship")]on a string). [AllowedRelations(typeof(ArticlePage), …)]— restrict which page types appear in the picker. Omit for any type.- Stores the target's ContentId (locale-agnostic) — resolve from the frontend via
GET /api/pages/content/{contentId}, then pick the translation for the locale you're rendering.
Attributes reference
| Attribute | Applies to | Options | Effect |
|---|---|---|---|
[CmsPage] | page class | DefaultSlug, Icon | Page metadata. DefaultSlug null = auto from name, "" = root. Icon = a built-in icon name ("house", "folder", "file"…) or any emoji; shows in the admin tree. |
[CmsGlobal] | class | — | Marks a singleton content type (site chrome like a header/footer) rather than a tree page. |
[AllowedChildren] | page class | params Type[] | Restrict child page types. Absent = any; empty = none; with types = only those. |
[CmsField] | property | DisplayName, FieldType | Override the editor label or force a property type alias (e.g. "textarea", "media", "relationship"). |
[AllowedRelations] | property | params Type[] | For relationship fields: which page types the picker lists. Absent or empty = any page type. |
[Localized] | property | — | This field gets a separate value per locale. |
[LocalizedPage] | page class | — | Every field on the page is localized. |
[Indexable] | property | — | Create a database index for this field (cross-adapter). |
Editorial features
Beyond the content model, the engine ships the workflow features editors expect:
Drafts & versioning
Edits go to a draft — the live page is untouched until you Publish. Full version history with one-click rollback, plus per-page scheduled publishing.
Roles & permissions
Capability-based roles (Administrator / Editor / Author). Authors save drafts; Editors publish. Enforced on the API and reflected in the admin UI.
Full-text search
Opt-in, storage-agnostic Lucene.NET index — tokenized and ranked, kept live via content events and rebuilt from the database on startup.
Webhooks & notifications
HMAC-signed webhooks for content changes, plus synchronous in-process notifications you can hook to mutate or cancel an operation (before/after publish, save, delete).
GraphQL (opt-in)
A read-only GraphQL delivery API over HotChocolate, mirroring the REST endpoints — added as a package, not in core.
Caching
Read-through page cache: in-process, Redis, or an L1+L2 HybridCache tier.
Extending Klassd
Custom property editors
A custom field editor is a single Blazor component — no JS, no registration. Inherit PropertyEditorBase and mark it with [PropertyEditor("alias")]; the engine discovers it by assembly scan and synthesises the property type for you.
@attribute [PropertyEditor("color")]
@inherits PropertyEditorBase
<input type="color" value="@Value"
@oninput="e => SetValueAsync(e.Value?.ToString() ?? string.Empty)" />Then reference the alias from any field:
[CmsField(FieldType = "color")]
public string BackgroundColor { get; set; } = "";PropertyEditorBase gives you Value (string), ValueChanged and the field metadata. The stored value is always a string — content is persisted DB-agnostically — and MediaReference / PageReference are typed wrappers over that string.
Custom storage & media adapters
Storage and media backends are swappable. The engine depends only on interfaces in Klassd.Abstractions — never a concrete database or cloud SDK. Implement IPageStore / IMediaStore (storage) or IBlobStore (media) and add a UseXxx registration extension. Worked examples ship in the examples/ folder.
Delivery API
The headless GET endpoints are anonymous so a public frontend can read published content (this site consumes them):
GET /api/pages— all pages for a locale (?locale=en).GET /api/pages/{id}— a single page.GET /api/pages/by-slug/{**slug}— a page by its slug (?locale=en).GET /api/pages/content/{contentId}— a page and its translations (resolve relationships here).GET /api/pages/{id}/translations— every locale of one page.GET /api/globals/{name}— a global singleton (e.g.SiteHeader).GET /api/media/{id}— a media item's bytes.
Only published, in-window content is delivered. Single-page GETs accept ?depth=1 to resolve PageReference/MediaReference fields to URLs and ?expand= to pick which. Scheduling resolves per request; ?preview=<utc> time-travels delivery when enabled. A /graphql endpoint is available via the opt-in GraphQL package.
Packages
Install the engine plus the adapters you need — each keeps its SDK isolated, so you only pull in what you wire up. While Klassd is in beta, add --prerelease.
| Package | Purpose |
|---|---|
Klassd.Abstractions | Storage/media interfaces + DB-agnostic POCOs (no dependencies). |
Klassd.Core | Content base types, attributes, registries, localization, default property types. |
Klassd.Backoffice | The engine — AddKlassd/UseKlassd, the Blazor admin, and the headless /api. |
Klassd.Data.Sqlite | SQLite storage adapter. |
Klassd.Data.Postgres | PostgreSQL storage adapter. |
Klassd.Data.MongoDb | MongoDB storage adapter. |
Klassd.Cache.InMemory | In-process read-through page cache. |
Klassd.Cache.Redis | Distributed (Redis) read-through page cache. |
Klassd.Cache.Hybrid | L1 + L2 cache over Microsoft.Extensions.Caching.Hybrid. |
Klassd.Media.FileSystem | Local-disk media blob store. |
Klassd.Media.S3 | Amazon S3 (or S3-compatible) media blob store. |
Klassd.Media.GoogleCloud | Google Cloud Storage media blob store. |
Klassd.Auth.OpenIdConnect | OIDC/OAuth SSO for the backoffice (SAML via the generic seam). |
Klassd.Search.Lucene | Opt-in full-text search over Lucene.NET. |
Klassd.Webhooks | Opt-in HMAC-signed content-change webhooks. |
Klassd.GraphQL | Opt-in GraphQL delivery API (HotChocolate). |
Klassd vs Umbraco vs Payload
How Klassd's code-first, .NET-native approach compares to two popular headless CMSs. All three are open-source (MIT).
| Feature | Klassd | Umbraco | Payload |
|---|---|---|---|
| Code-first schema | C# classes | UI / database-driven | TypeScript config |
| Admin UI | Blazor (no JS build) | Web-components SPA | React (in your Next.js app) |
| Drafts & versioning | Yes | Yes | Yes (+ autosave) |
| Scheduled publishing | Yes | Yes | Yes |
| Roles & permissions | Capabilities + roles | User groups | Access-as-code |
| Full-text search | Lucene.NET (opt-in) | Examine / Lucene | Plugin |
| Webhooks / events | Webhooks + notifications | Notifications | Hooks |
| REST delivery | Yes | Yes | Yes |
| GraphQL | Opt-in package | Cloud (Heartcore) only | Core |
| Storage backends | Mongo / Postgres / SQLite | SQL Server / SQLite | Mongo / Postgres / SQLite |
| Localization | Per-field + markets | Culture + segment variants | Field-level locales |
| Runtime / platform | .NET (Blazor) | .NET (ASP.NET Core) | Node (Next.js) |
| License | MIT | MIT | MIT |
Klassd's niche: true code-first content modelling in C#, a Blazor admin with no JS build step, and pluggable storage — shipped as NuGet packages you compose. Comparison reflects each project's core/open-source offering as of 2026.