Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

The CMF Landscape -- Standing on Giants

Origin Pattern Adopted What Changed
Diem (Symfony 1.4, 2010) Two-DSL approach, Page/Zone/Widget, "starts empty" PHP → C#, YAML → attributes, runtime → compile-time
Wagtail (Django) StreamField, typed content blocks as JSON Python → C#, runtime blocks → compiled block types
Orchard Core (.NET) Content Parts as composable mixins Runtime composition → compile-time [HasPart]
Drupal Content versioning, editorial workflows, taxonomy PHP hooks → Workflow DSL with typed state machines
Symfony CMF Dynamic routing from content tree PHPCR → EF Core page tree with materialized paths
Strapi Auto REST + GraphQL from schema Node.js → Roslyn source generators

What this CMF adds beyond all of them: DDD as a first-class concern (bounded contexts, aggregates, sagas, CQRS), an M3 meta-metamodel making all DSLs self-describing, and type safety across the full stack via the shared C# kernel.

Why Each Pattern, and Why Not Its Alternatives

The table above is a list of adoptions, not a survey. Each pattern was chosen against a specific alternative that the predecessors had also tried. Recording the alternatives — and the reasons they were rejected — is more useful than the adoption list itself.

Diem's "starts empty" philosophy vs. WordPress's "starts full". WordPress ships with posts, comments, taxonomies, themes, and a plugin marketplace. The developer's job is to subtract what they don't want and override what doesn't fit. Diem inverted this: zero entities, zero modules, zero pages — the developer adds. The CMF keeps Diem's stance because subtraction is dangerous (a removed plugin can break a deployed instance) while addition is reversible. The cost is that the first hour of a Diem (or this CMF) project is slower than the first hour of a WordPress project. The benefit is that the next thousand hours are faster, because there is no inheritance from defaults the developer never asked for.

Wagtail's StreamField vs. Drupal's Paragraphs / WordPress's Gutenberg blocks. All three solve the same problem: long-form content composed of typed sub-components. StreamField stores the sequence as a JSON list of (type, data) pairs, which is easy to query, easy to migrate, and easy to render in any frontend. Gutenberg embeds blocks as HTML comments inside the post body, which entangles content with presentation and makes querying impractical. Paragraphs uses separate DB tables per block type, which gives you SQL but multiplies the schema. The CMF picks the StreamField approach because the JSON document maps naturally to a generated discriminated-union C# type, which is then both SQL-friendly (Postgres jsonb) and trivially serializable to the WASM client.

Orchard Core's Content Parts vs. Drupal's hook system. Both let cross-cutting concerns (routing, SEO, taxonomy, versioning) be attached to a content type without changing that type's definition. Orchard does it via composition (HasPart<RoutablePart>), Drupal via runtime hooks (hook_node_load, hook_entity_view). Composition is checkable at compile time; hooks are not. The CMF takes Orchard's approach but moves it from runtime composition to compile-time [HasPart] so the analyzers can verify part compatibility before the application runs.

Drupal's editorial workflows vs. ad-hoc state columns in WordPress. Drupal recognized early that "draft / review / published" is a state machine, not three boolean columns. The CMF extends this by making the state machine a typed declaration ([Workflow] [Stage] [Transition]) rather than a configuration document, so transitions are reachable via IDE go-to-definition and the unreachable-state warnings live in the analyzer family CMF5xx.

Symfony CMF's content-driven routing vs. Rails-style route files. Rails forces every URL into a route table at boot time, which is great for APIs and terrible for content-heavy sites where the editor adds pages daily. Symfony CMF flipped this by deriving the URL from a node's position in a content tree. The CMF adopts the same idea but stores the materialized path in Postgres rather than PHPCR, so a single SQL index lookup resolves any URL — including arbitrarily deep page trees — in O(log n).

Strapi's auto-generated REST + GraphQL vs. hand-rolled controllers. Strapi proved that schema-driven API generation is viable for production. The CMF goes one step further: the schema is an attributed C# class instead of a JSON document, which means the API contract is type-checked, the DTOs round-trip through the shared kernel without translation, and OpenAPI metadata can be enriched with [Implements] tags from the requirements DSL. The trade-off is that the developer has to write C#; the upside is that they cannot ship a contract that doesn't compile.

Failure Modes the CMF Is Designed to Avoid

Each predecessor has a known failure mode. Documenting them is the most honest way to position this CMF — not as "better", but as "different in specific, falsifiable ways".

Predecessor Failure mode How the CMF avoids it
Diem (PHP/Symfony 1.4) No type safety; renaming a column in schema.yml broke everything in production at runtime Roslyn analyzers catch every renamed property at compile time across the full stack
Wagtail (Python/Django) Python-only ecosystem; no client-side sharing of types; runtime type errors during template rendering Shared C# kernel compiles to both server and WASM; rendering errors are compile errors
Orchard Core (.NET/Razor) Runtime composition is dynamic — the developer cannot tell what is on a content type without booting the app and inspecting the admin UI [HasPart] is static; the IDE shows every composed part on a single line
Drupal (PHP) Hook callbacks scattered across modules; debugging "where is this saving the data" requires xdebug + a dozen breakpoints Source-generated handlers are concrete C# types with one entry point each
Strapi (Node.js) Weak DDD; aggregates are flat schemas with no invariants; cross-collection consistency is the developer's problem DDD DSL is the first layer; aggregates own their invariants and the generator wires them into every command path
WordPress Plugin conflicts at runtime; the same hook is registered by three plugins with incompatible expectations The CMF has no runtime plugin model — extension is via additional source generators that participate in compile-time

This is not a claim of superiority: WordPress's runtime plugin model makes one-click installs possible, which the CMF will never offer. The trade is deliberate. A CMF that demands a dotnet build between feature toggles is unsuitable for marketing sites that change hourly; it is suitable for line-of-business applications where wrong is more expensive than slow.

Migration Path from Diem 1.x

A non-trivial number of projects from the 2010–2014 era are still running on Diem. For those teams, the migration arc is:

  1. Lift the schema.yml. Each Doctrine model becomes a partial class with [AggregateRoot] and one [Property] per column. A small cmf import diem-schema command does the mechanical translation; the developer reviews the cardinality decisions (Diem's relations: block does not distinguish composition from association).
  2. Lift the modules.yml. Each Diem module declaration becomes an [AdminModule] attribute on a marker class. Field-level customizations (Diem's form: and list: keys) map to [AdminField] overrides.
  3. Lift the page tree. Diem stores its page tree in dm_page and dm_zone tables; the CMF's pages DSL uses a near-identical schema (materialized path + zone widgets), so the migration is a SQL INSERT ... SELECT plus a one-time renumbering of the materialized paths.
  4. Lift widgets selectively. Most Diem widgets (Lists, Show, StaticHTML) have direct [PageWidget] equivalents. Custom widgets — anything that Diem developers wrote in PHP — must be re-implemented in Blazor.
  5. Run side-by-side. Both the Diem instance and the CMF instance can coexist behind a reverse proxy that routes by URL. As pages are migrated, the proxy rewrites their entries from diem.local to cmf.local. Once the page tree is empty on the Diem side, decommission.

The full migration of a medium Diem project (≈40 content types, ≈200 pages) is on the order of weeks of focused work, not months. The CMF is not a drop-in replacement — the runtime models are different — but the conceptual mapping is one-to-one, which is the whole reason this CMF exists.

⬇ Download