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 />
<Plugin.Tooltip.V2 />
<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
└── tabboard.js
What happens at install time
- The zip is validated against the manifest schema.
- The DLL is loaded into a Collectible Assembly Load Context — isolated from the host and garbage-collected when replaced.
- Static assets are mounted at
/_ideas/Component/tabboard/1/.
- The widget is registered in the in-memory content catalog.
- 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.