CMS — a multi-site, block-based content backend
Granit.Cms is a headless, multi-site CMS built on the Granit framework. The backend
owns content, structure, versioning, and SEO; a Next.js App Router front renders pages
server-side with ISR. Editors compose pages from blocks in a Puck editor — the backend
stores the block tree as opaque JSON and never parses it, so the renderer and the editor
evolve without backend changes.
It is delivered as the Granit.Cms.* package family (shipped from the granit-website
repo, consumed via NuGet), each capability an opt-in module with the usual Granit layer
split (.EntityFrameworkCore, .Endpoints, .BackgroundJobs).
What it does
Section titled “What it does”| Capability | Module | What it gives you |
|---|---|---|
| Sites & pages | Granit.Cms | Multi-site tree, copy-on-write page versions, draft→publish, i18n routing, preview, menus |
| Blocks | Granit.Cms | Self-describing block registry + catalog, server/client render, data-bound blocks |
| Releases | Granit.Cms | Schedule a batch of publish/unpublish actions at a wall-clock time |
| SEO | Granit.Cms.Seo | Cascading metadata, sitemap/robots/manifest, JSON-LD — plus AI-suggested titles & descriptions |
| Redirects | Granit.Cms.Redirects | Per-site 301/302/307/308 with prefix matching, loop-safe chains, auto-redirect on page move |
| Custom domains | Granit.Cms.Hostnames | Map a verified hostname to a site (DNS challenge + certificate lifecycle) |
| Media references | Granit.Cms.Documents | Resolve documents embedded in blocks into stable, renewable asset URLs |
Architecture at a glance
Section titled “Architecture at a glance”flowchart LR
Editor["Puck editor (front)"] -->|draft JSON| API["Granit.Cms API"]
API -->|jsonb| DB[("Postgres<br/>cms_* tables")]
Renderer["Next.js SSR/ISR"] -->|by-path, published| API
Visitor["Visitor"] -->|hostname| Renderer
API -. resolves .-> SEO["Granit.Cms.Seo"]
API -. resolves .-> RD["Granit.Cms.Redirects"]
API -. resolves .-> HN["Granit.Cms.Hostnames"]
Two design rules shape everything:
SiteIdis the multi-site discriminator;TenantIdis the GDPR boundary. Every CMS entity carries both. A tenant can run N sites; site-scoped query filters keep one site’s content invisible to another, while tenant isolation stays the compliance edge.- Copy-on-write versioning. A
Pageis stable structure; its content and publication state live on immutablePageVersionrows (Draft → Published → Archived). No event sourcing — just a new draft forked from any prior version. See Sites & Pages.
Each capability is a module you depend on, an EF Core registration, and a Map call. The
core is the minimum; add satellites as needed.
// Module graph (DependsOn on your app module)[DependsOn( typeof(GranitCmsModule), typeof(GranitCmsEntityFrameworkCoreModule), typeof(GranitCmsEndpointsModule), typeof(GranitCmsBackgroundJobsModule))]public class AppModule : GranitModule { }// Program.cs — persistence (isolated, tenant + site aware DbContext)builder.AddGranitCmsEntityFrameworkCore(db => db.UseNpgsql(connectionString));builder.AddCmsSeoEntityFrameworkCore(db => db.UseNpgsql(connectionString)); // optionalbuilder.AddCmsRedirectsEntityFrameworkCore(db => db.UseNpgsql(connectionString)); // optionalbuilder.AddGranitCmsDocumentsEntityFrameworkCore(db => db.UseNpgsql(connectionString)); // optional// Map the HTTP surfaceapp.MapGranitCms(); // sites, pages, blocks, menus, releases, routing, previewapp.MapGranitCmsSeo(); // sitemap/robots/manifest + metadata adminapp.MapGranitCmsSeoAI(); // AI suggestion inboxapp.MapGranitCmsRedirects();app.MapGranitCmsSiteHostnames();app.MapGranitCmsDocuments(); // anonymous asset redirect endpointLocked architectural decisions
Section titled “Locked architectural decisions”These are settled in the backend and reflected throughout the docs:
- Editor: Puck (JSON block tree) + TipTap rich text inside blocks.
- Renderer: Next.js App Router, SSR + ISR (tag-based cache invalidation).
- Multi-site: N sites per tenant;
SiteIdon every entity. - i18n: sub-path routing (
/fr/,/en/), per-culture URL slugs. - Versioning: copy-on-write per page + audit interceptor. No event sourcing.
- Preview: signed HMAC token on a draft; never touches the ISR cache.
- Blocks: server-rendered by default; client only when declared.
- Persistence: Postgres-first,
jsonbfor serialized block content.
See also
Section titled “See also”- Sites & Pages — the content core
- Blocks · Releases · SEO
- Redirects · Custom Domains · Media References
- Persistence — the isolated DbContext pattern these modules use