A Content Management System for the post-framework era

MindAttic.Ideas

Upload a .idea package. It goes live. No restart. No pipeline. No ceremony.

Hot-load Zero-redeploy Scoped CSS & JS Free-form pages .NET 10 / Blazor

Three honest primitives

A CMS has exactly one job: connect content to presentation without getting in the way. That takes three things and nothing else.

Page

Free-form HTML in a database row

One table. One row per page. No zones. No panes. No layout XML. A page is a slug, a title, and a body that can embed widget tokens anywhere in the markup. The browser <title> is its own standalone field. That's it.

Plugin / Component

A self-contained .idea zip

A single archive: a compiled Blazor component, a CSS file scoped to a widget-specific class prefix, and JavaScript in an IIFE. Installed live with no redeploy. Versioned with a whole number — V1, V2, never 2.6.0.

Theme

A visual wrapper — nothing more

Themes control typography, color, and the chrome around a page's body. They never touch the admin UI. They never own routes. A broken theme cannot lock you out of your site.

The token grammar

Compose a page by writing free-form HTML and dropping tokens wherever a widget should appear. No visual builder required. No drag-and-drop. No layout system.

A page body (authored in the admin editor):

<h2>Software Projects</h2>
<Component.TabBoard />

<h2>Selected Work</h2>
<Component.Gallery />

<!-- Pin a specific version: -->
<Plugin.Tooltip.V2 />

<!-- Pass attributes to a widget: -->
<Component.Hero title="Hello" />

Resolution rules:

  • Floating — no version suffix → latest installed version
  • Pinned — .V2 suffix → that exact version, always
  • Missing → a placeholder renders instead of a crash; the admin inbox gets an alert
  • Disabled → same graceful degradation; the page still serves
  • Attributes → passed directly to the Blazor component's [Parameter] properties

Anatomy of a .idea package

Every widget is one file. Drop it in Admin → Upload and it is live within the same request cycle — no IIS restart, no config change, no app pool recycle.

MindAttic.Ideas.Component.TabBoard.V1.idea  (zip)
├── idea.json
│   {
│     "key": "tabboard",
│     "version": 1,
│     "displayName": "Tab Board",
│     "category": "Component"
│   }
├── bin/
│   └── MindAttic.Ideas.Component.TabBoard.dll
└── wwwroot/
    ├── tabboard.css  ← .ma-tabs-*  (never leaks)
    └── tabboard.js   ← (function(){ … })()  (no globals)

What happens at install time

  1. The zip is validated against the manifest schema.
  2. The DLL is loaded into a Collectible Assembly Load Context — isolated from the host and garbage-collected when replaced.
  3. Static assets are mounted at /_ideas/Component/tabboard/1/.
  4. The widget is registered in the in-memory content catalog.
  5. Any page containing <Component.TabBoard /> resolves it on the next render — without touching the page record.

They saw the future. The technology wasn't ready.

DotNetNuke launched in 2003 and had a genuinely revolutionary idea: a CMS where every capability arrived as an installable zip package. Upload a module, it's live. No developer required. No deployment pipeline. No server access needed. They saw exactly what the web should be — and they were right.

But the technology of the era made clean execution impossible. WebForms required every module to participate in an elaborate page-event lifecycle. CSS had no isolation mechanism. JavaScript had no module system. And most fatally, the Skin system wrapped the entire page including the admin chrome. A broken skin meant you couldn't access your own admin panel to fix it. You were locked out of your own site.

The concept was correct. The execution was a prisoner of 2003.

MindAttic.Ideas revisits that vision with two decades of better tools. The installable package idea is intact. Everything else is solved:

  • Blazor's component model gives widgets genuinely isolated render trees
  • A per-widget CSS class prefix means .ma-tabs-* cannot affect .ma-footer-*
  • JavaScript in an IIFE means no global variable conflicts between widgets
  • The admin UI lives on its own route prefix, styled by a fixed system stylesheet — never a theme
  • Collectible Assembly Load Contexts mean a widget DLL is isolated and garbage-collected when replaced
  • Pages are free-form HTML — no zone/pane/slot XML, because HTML and CSS already solve layout

What DNN got right

  • Capabilities as installable zip packages
  • Hot-install without server restarts
  • Content independent of presentation
  • Portal-based multi-tenancy vision
  • A module marketplace concept
  • Versioned modules

What held it back

  • Themes wrapped the admin UI (fatal)
  • No CSS isolation between modules
  • No JS isolation — globals everywhere
  • Zone/pane layout system instead of free HTML
  • WebForms postback lifecycle complexity
  • Module API tightly coupled to DNN internals
  • Technical debt outpaced the vision

How it stacks up

Feature DNN WordPress Orchard Core Umbraco MindAttic.Ideas
Hot-install packages (no redeploy) ✗ ✓ ✗ ✗ ✓
Free-form HTML pages (no zones or panes) ✗ ~ ✗ ~ ✓
CSS isolated per widget (no global leakage) ✗ ✗ ✗ ✗ ✓
JS isolated per widget (IIFE, no globals) ✗ ✗ ✗ ✗ ✓
Theme never wraps admin UI ✗ ~ ✓ ✓ ✓
Token grammar for inline widget composition ✗ ~ shortcodes ✗ ✗ ✓
Per-page temporal history (every prior state recoverable) ✗ ~ revisions ✗ ✓ ✓
Modern .NET (Blazor server, .NET 10) ✗ ✗ ✓ ~ ✓
Bridge tables over JSON blobs for structured data ✗ ✗ ✗ ✗ ✓
Whole-number versioning (V1, V2 — never 1.5.11) ✗ ✗ ✗ ✗ ✓

✓ = Yes  ·  ✗ = No  ·  ~ = Partial or with caveats

Under the hood

SQL Server temporal tables

Every page row is system-versioned. Any prior state — including which widget versions it referenced — is recoverable. Wiki-like history with no separate history table.

Entity Framework Core

No raw SQL in application code. Bridge tables everywhere — one row per fact, no JSON blobs for structured data. Migrations are additive and never destructive.

Collectible Assembly Load Contexts

Each widget DLL loads in its own ALC, isolated from the host. Replacing a widget unloads the old DLL and loads the new one. The host process never restarts.

Single Blazor circuit per page

One interactive circuit. Every widget on a page shares it. No per-widget SignalR connection, no per-widget app pool. Interactive where needed, static where not.

Content trust model

Inline JavaScript only executes when the author holds the Cms.AuthorRawMarkup claim. Untrusted HTML is sanitized at render time. Trust is stamped at write time, not read time.

Admin inbox

Every missing, disabled, or misconfigured widget surfaces a deduplicated alert in the admin inbox — the page degrades gracefully and the problem is never silent.

Get started in four steps

1
Create a page

Admin → Pages → New Page. Set a slug and a title.

2
Add a widget token

Type <Component.TabBoard /> into the body editor.

3
Upload the widget

Admin → Upload → drop MindAttic.Ideas.Component.TabBoard.V1.idea.

4
Navigate to the page

The widget resolves on the next request. No restart. No cache clear. No config change.

Try the live demo

A real Ideas install, not a video. Sign in as the admin, upload an .idea, build a page, switch themes. It wipes itself and gets a brand-new password every hour, so break anything you like.

https://mindattic-ideas-demo.azurewebsites.net

Demo sign-in opens soon.

MindAttic.Ideas  ·  Built on .NET 10  ·  © 2026 Ryan DeBraal

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.