Skip to content

getklassd/Klassd

Repository files navigation

Klassd

License: MIT .NET 10 CI Status: Beta

⚠️ Beta

Klassd is in public beta (0.0.x). It builds, is covered by unit/integration/UI tests, and runs — but it's young: the API surface may change between releases until 1.0, and you may hit rough edges. Pin your package versions, read the release notes when upgrading, and please open an issue for anything that looks off. Not yet recommended for production without your own evaluation.

A code-first, NuGet-distributed headless CMS for .NET. You define your content model — pages, blocks and property types — as plain C# classes. The engine reflects over them to drive a Blazor (Interactive Server) admin at /admin and a headless JSON API at /api. No content-type designer, no database migrations to hand-write, no JavaScript build step.

Your content schema lives in your codebase, versioned with your app and refactored with your IDE.

Why Klassd

  • Code-first — content types are C# classes; rename a property in your IDE, not a CMS UI.
  • Headless — public JSON delivery API; render with any frontend (or none).
  • Pluggable storage — MongoDB, PostgreSQL or SQLite via a single .UseXxx(...) call.
  • Pluggable media — file system, Amazon S3 or Google Cloud Storage, with named sections.
  • Localization built in — per-locale fields via [Localized], market-local scheduling.
  • No JS toolchain — the admin is Blazor; cloud SDKs stay isolated in their own packages.

The admin

The Blazor admin at /admin is generated from your C# content types — no JavaScript build step, no separate schema to maintain. Pages, blocks, fields, media and localization all come from your code.

Sign in Page tree
Sign in Pages
Page editor (fields + blocks) Media library
Page editor Media
Users Dictionary
Users Dictionary

…and the same shell in dark mode (toggled per user, persisted to preferences):

Page tree Page editor Media
Pages (dark) Page editor (dark) Media (dark)

The page editor above is fully driven by the HomePage/HeroBlock C# classes — the Title field, the Hero Blocks area, and per-block scheduling are all reflected from your model.

Quickstart

Install the engine plus one storage adapter. While Klassd is in beta the packages are prerelease, so pass --prerelease (or pin an explicit version):

dotnet add package Klassd.Backoffice --prerelease
dotnet add package Klassd.Data.Sqlite --prerelease

1. Define content types as C# classes (discovered automatically from your app's assembly):

using Klassd.Core.Abstractions;

[CmsPage(DefaultSlug = "", Icon = "house")]   // Icon shows in the admin tree (built-in name or any emoji)
[AllowedChildren(typeof(ContentPage))]
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 HeroBlock : BlockBase
{
    public string Heading { get; set; } = "";

    [CmsField(FieldType = "media")]   // media picker; stores the media item id
    public string Image { get; set; } = "";
}

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();
// appsettings.json
"Sqlite": { "ConnectionString": "Data Source=klassd.db" }

Host .csproj — one required setting. The Blazor admin ships entirely inside the Klassd.Backoffice package, so a host that has no .razor files of its own must opt in to the Blazor framework assets, or /admin 404s on _framework/blazor.web.js and never goes interactive:

<PropertyGroup>
  <RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>
</PropertyGroup>

This can't be set by the package itself: the property gates a restore-time framework download, and NuGet restore does not read a referenced package's MSBuild props (it would be circular), so a value Klassd set would arrive too late. It must live in your host project. (If your host already has its own .razor files, the SDK turns this on automatically and you can omit it.)

3. Run and open /admin. The public site reads published content from /api/pages.

See src/Klassd.Sample for a complete runnable host with multiple page/block types, a custom property editor, media sections and SSO examples.

Media

Media is organized into named sections, each backed by its own blob adapter. Add a section with .AddMedia(...) after choosing storage, and reference it from a field with [CmsField(FieldType = "media")] (the admin renders an upload + picker; the field stores the item id):

dotnet add package Klassd.Media.FileSystem   # and/or .Media.S3, .Media.GoogleCloud
builder.Services
    .AddKlassd(builder.Configuration)
    .UseSqlite(builder.Configuration.GetSection("Sqlite"))
    .AddMedia(media =>
    {
        // Local disk — longest image edge downscaled to 2000px in-browser before upload
        media.AddSection("images", s => s
            .UseFileSystem(Path.Combine(builder.Environment.ContentRootPath, "media", "images"))
            .AllowContentTypes("image/*")
            .ResizeImages(2000)
            .Breakpoints("default", "mobile", "tablet", "desktop"));  // focal-point breakpoints

        // Amazon S3 (or any S3-compatible backend via ServiceUrl/ForcePathStyle)
        media.AddSection("documents", s => s
            .UseS3(o =>
            {
                o.Bucket = "my-cms-docs";
                o.Region = "eu-west-1";   // omit AccessKey/SecretKey to use the default AWS credential chain
            })
            .AllowContentTypes("application/pdf"));

        // Google Cloud Storage
        media.AddSection("video", s => s
            .UseGoogleCloudStorage(o =>
            {
                o.Bucket = "my-cms-video";
                o.CredentialsPath = "/secrets/gcs.json";   // or CredentialsJson, or ambient ADC
            }));
    });

Each section is independent: mix local disk, S3 and GCS in the same app, set per-section allowed content types, downscale images on the client with ResizeImages(maxEdgePixels), and declare the focal-point Breakpoints(...) editors pick from (a single "default" when unset).

Need a backend we don't ship (Azure Blob, an in-house store, …)? A media adapter is just an IBlobStore (three methods) plus a UseXxx extension. See examples/InMemoryMediaAdapter for a complete, annotated walkthrough.

Custom adapters

Klassd's storage and media backends are swappable extension points — the engine depends only on interfaces in Klassd.Abstractions, never on a concrete database or cloud SDK. To target a backend we don't ship, implement the relevant interface and add a UseXxx registration extension. Worked, compilable examples live in examples/:

Example Implements Extension point
InMemoryMediaAdapter IBlobStore UseInMemoryBlobs() on a media section
InMemoryStorageAdapter IPageStore, IMediaStore, IDictionaryStore, IUserStore, IPreferencesStore, IUnitOfWork, IStorageInitializer UseInMemoryStorage() on the CMS builder

Content delivery & CORS

The headless GET delivery endpoints are anonymous so a public frontend can read published content without credentials:

  • GET /api/pages, /api/pages/{id}, /api/pages/content/{contentId}, /api/pages/{id}/translations
  • GET /api/dictionary/resolved/{locale}
  • GET /api/media/{id}

Everything else — page/media/dictionary management, users, preferences, and the /admin UI — still requires the admin cookie.

Restrict which browser origins may fetch via JS with config (empty/unset ⇒ any origin):

"Klassd": {
  "Cors": { "AllowedOrigins": [ "https://www.example.com", "https://shop.example.com" ] }
}

CORS only limits cross-origin browser requests; it is not an authorization boundary for server-side callers. Delivery content is genuinely public — don't put gated content in it.

Optional: gate delivery with an API key

This is a headless CMS, so the rendering model is the consumer's choice. If your frontend renders server-side (SSR/SSG/BFF), you can require an API key on the delivery GETs — the key stays on your server, never in the browser:

"Klassd": {
  "Delivery": { "RequireApiKey": true, "ApiKey": "<long-random-secret>", "ApiKeyHeader": "X-Api-Key" }
}

Callers then send X-Api-Key: <secret>; requests without it get 401. Default is off (public).

Do not enable this for a browser/SPA that calls the CMS directly — the key would ship in the client bundle and provide no real protection. For a client-side app that needs gating, put a backend-for-frontend (BFF) in front that holds the key, or use per-user auth.

Packages

Package Purpose
Klassd.Abstractions Storage adapter interfaces + DB-agnostic POCOs (no deps)
Klassd.Core Content base types, attributes, registries, localization, default property types
Klassd.Backoffice The engine: AddKlassd/UseKlassd, Blazor admin, headless /api
Klassd.Data.MongoDb / .Data.Postgres / .Data.Sqlite Storage adapters
Klassd.Cache.InMemory / .Cache.Redis Read-through page cache adapters
Klassd.Media.FileSystem / .Media.S3 / .Media.GoogleCloud Media blob adapters
Klassd.Auth.OpenIdConnect OIDC/OAuth SSO for the backoffice (SAML via the generic seam)

The engine package carries no MongoDB/AWS/Google dependency — each adapter keeps its SDK isolated, so you only pull in what you wire up.

Deployment notes

Time zones (content scheduling)

Block scheduling is market-local: each locale carries an IANA TimeZone (e.g. Europe/Berlin, Asia/Dubai) and editors author schedule times as wall-clock time in that market. Times are stored and compared in UTC, so delivery is correct across markets simultaneously.

Resolving IANA time zones requires the OS time-zone database:

  • Debian/Ubuntu base images (incl. the default mcr.microsoft.com/dotnet/aspnet) include it — nothing to do.

  • Alpine images do not. Add it:

    RUN apk add --no-cache tzdata
  • Chiseled / distroless: use the -extra image variant (ships ICU + tz data), not the bare one.

If a configured locale time zone can't be resolved at startup, the engine logs a warning (category Klassd.Scheduling) naming the locale and zone, and falls back to UTC for that market (so scheduling would be offset-wrong). Watch for that warning when deploying to slim images.

Note: this is about the time-zone database, not InvariantGlobalization — leaving globalization invariant does not remove the tz-data requirement.

Building & testing

dotnet build Klassd.slnx -c Release

Tests use TUnit on the Microsoft.Testing.Platform. On the .NET 10 SDK, dotnet test does not work for these — run each project directly:

dotnet run --project tests/Klassd.UnitTests -c Release
dotnet run --project tests/Klassd.IntegrationTests -c Release   # needs Docker (Testcontainers); container tests auto-skip without it
dotnet run --project tests/Klassd.UiTests -c Release            # needs Playwright browsers (see below)

UI tests are Playwright E2E; first run installs the browser:

pwsh tests/Klassd.UiTests/bin/Release/net10.0/playwright.ps1 install chromium

Security

See SECURITY.md for the vulnerability reporting process and important notes on the public delivery endpoints.

Built with AI

Klassd was built largely with AI assistance — Claude Code (Anthropic's Claude) was used throughout for design, implementation, refactoring and tests, working alongside a human maintainer who reviews and directs the work. The architecture, content model and adapter design were shaped through that collaboration, and most commits are co-authored accordingly.

It's called out here for transparency: read the code with the same scrutiny you'd give any dependency, and please report anything that looks off via the issues or SECURITY.md.

Acknowledgements

Klassd stands on excellent open-source work. Thank you to the maintainers of:

  • daisyUI (MIT) — the component layer the admin UI is built on. Vendored as Klassd.Backoffice/wwwroot/daisyui.css (the file keeps its license header), so no build step is required.
  • Tailwind CSS (MIT) — the utility/design-token foundation daisyUI is built on.
  • Lucide (ISC) — the page-type / UI icon set (TypeIcon).
  • Vue + Vite (MIT) — the SSR frontend and its build.
  • Bun (MIT) — the frontend runtime/server.
  • Playwright (Apache-2.0) and TUnit (MIT) — the test stack.
  • .NET & Blazor (MIT) — the platform Klassd is written on.

License

MIT © Mark Lonquist

About

Code-first, NuGet-distributed headless CMS for .NET — define content types as C# classes, get a Blazor admin + headless API.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors