HCTI turns HTML and CSS into images through an API. Our template editor lets customers design that HTML visually, decide which values can be supplied later, and render the saved template with JSON. The product introduction has more on what the editor does; this post covers how we built it.
Background
HTML/CSS to Image is built in C#. We have one monolith deployed in five service configurations, with a Cloudflare Worker at the front making the real routing decisions. Most of our website is server-rendered with Blazor Static, with HTMX and small interactive components where they make sense.
We had wanted to offer an interactive, design-focused editor for years. The visual part was never the only obstacle. Drawing blocks and showing property panels matters, but the harder question is what those edits mean after a customer saves the template, renders it through the API and returns months later expecting the same result.
A block template touches almost every part of HCTI: browser interaction, model design, validation, storage, API communication, HTML/CSS generation, Chromium rendering and testing. We needed an architecture that could keep those pieces aligned without maintaining a second version of the product in the browser.
Core Constraints
A few requirements shaped the editor from the beginning:
- This is a template editor, not an image editor. Customers must be able to choose which values are replaced later through the API.
- Editing needed to feel immediate. Color drags, spacing changes, keyboard nudges and variable previews could not wait for a server render.
- Advanced HTML and CSS behavior should remain available without requiring raw code for ordinary work.
- Saved output needed to be deterministic and testable. "It looked right in my browser" is not enough for a template that may keep rendering for years.
- Adding a normal property should not require a checklist spread across the model, sidebar, preview system, CSS renderer and tests.
Those constraints pushed us toward a typed model with generated editing infrastructure around it. The model represents the saved template. Editor-only behavior, such as preview values and undo history, stays around that model without changing the public JSON payload customers use at render time.
Why Blazor WebAssembly?
There are plenty of good editor foundations in the JavaScript ecosystem. FabricJS, Konva and the usual React-based options all provide useful starting points. Even so, we kept coming back to Blazor WebAssembly.
For readers outside the .NET world, Blazor WebAssembly is client-side web development in C#. Components, event handlers and application logic are written in C# and compiled to run directly in the browser. The frontend can use the same models, validation and domain libraries as the backend, which is a pretty compelling option when most of your product already lives in C#.
C# is familiar to us, but familiarity was not enough on its own. Most of the editor's value lives in the template model, validation, variable rules, serialization, CSS production and server rendering. Maintaining those rules once in C# and again in a TypeScript model would invite years of small disagreements.
Blazor lets the browser editor share the domain layer with our servers. Unlike Blazor Server (static or interactive), though, the WebAssembly application cannot call server code directly. Loading media, fonts or saved templates has to cross an API, so transport and authentication become part of the editor architecture.
We still use TypeScript where the browser has the advantage. Pointer interactions, geometry measurements, rich text, Monaco and other browser-facing work do not become better merely because they could be written in C#. The useful boundary is that TypeScript owns temporary browser interaction state while Blazor owns the application and committed template model.
Final Architecture
The editor is a Blazor WASM application mounted inside our existing Blazor Static site. The rest of the dashboard remains server-rendered, while the editor loads only where it is needed.
| Component | Responsibility | Description |
|---|---|---|
| Blazor Static page | Hosting the editor | Renders the page, mount point, boot scripts, loading shell and initial configuration. |
| Shared C# model | Template source of truth | Defines the canvas, blocks, typed values, variables and rendering rules used across browser and server. |
| Blazor WASM client | Editor application | Renders blocks, property panels, selection, validation, history and the rest of the editing UI. |
| Generated tracking layer | Editing the model | Routes typed previews and commits through compact paths, then groups committed changes into transactions. |
| TypeScript interaction layer | Browser interactions | Handles dragging, resizing, rotating, dropping and temporary DOM transforms before committing back to .NET. |
| Generated CSS layer | Visual output | Produces normal and preview styles from the model for both the editor and final server renderer. |
| Roslyn source generators | Repetitive infrastructure | Emit property descriptors, tracking routes, preview properties, variable traversal and CSS methods during compilation. |
| HCTI servers | Storage and editor APIs | Store templates with MemoryPack, repair older schemas and expose typed operations through MagicOnion. |
| HCTI renderer | Final image | Combines the model with customer JSON, writes HTML/CSS and renders the result through Chromium. |
The editor and renderer do not share identical DOM trees. They share the model, generated declarations and CSS target contract that define what those trees mean. Likewise, TypeScript does not directly mutate the saved model. Final interaction results return to .NET and enter the same transaction system used by property controls.
Roslyn source generators are heavily involved, but nothing is generated in the browser. They run during compilation, inspect our attributes and partial types, and emit normal C# that is built into the application.
Technical Deep Dives
Building Consistent Blazor UX with Source Generators
The typed model drives generated property descriptors, shared input workflows and common block-rendering contracts. This keeps ~180 addressable properties consistent without asking source generation to design the actual Razor UI.
Undo, Redo, and Live Preview - No Diffs Needed
Compact generated addresses route edits without reflection or string paths, while transactions preserve the user's intent across multi-property changes. Preview state follows similar routes but stays out of the saved model and undo history.
Why Our Blazor App Still Uses TypeScript
Blazor owns the application and committed state; TypeScript owns high-frequency browser behavior while an interaction is active. Typed callbacks and commit events let the two runtimes cooperate without competing over the same state.
Beyond Text Replacement: Building Typed Template Variables
Handlebars remains the content language, while colors, sizes, images and other properties gain typed variable bindings and explicit requirement modes. A shared inventory combines generated property traversal with Handlebars discovery and resolves the same nested JSON used by the public API.
Building a System to Generate Complex CSS
A generated CSS pipeline maps typed model properties onto stable targets shared by the editor and server renderer. Normal declarations stay automatic, while target deciders and custom emitters handle the CSS that needs more context.
Simplifying Complexity: MemoryPack to the Rescue
MemoryPack keeps the deep model compact across storage and browser transport, while read-time repair handles schema evolution close to the affected types. MagicOnion carries the same C# contracts over gRPC-Web without introducing another DTO system.
Productionizing Our Blazor WebAssembly Editor
Runtime ownership, narrow component updates and generated routing keep the running editor responsive. Trimming, startup preloading, a deliberate loading shell and layered browser tests address the production costs around the application.
What's Next?
We plan to keep writing about the engineering behind the editor and the rest of HCTI. A few topics we may cover in future technical posts:
- The source generators behind HCTI
- How we use infrastructure as code in C# for strongly typed, secure configuration
- Our Cloudflare Worker setup
- Building template presets with the help of AI
- How we mix serverless and servers for queueless, unlimited Puppeteer-ing
Which one would you like to read first? If there is another part of HCTI you are curious about, let us know by email or Twitter.
Template Editor Engineering Series
| Post | Description |
|---|---|
| Building a Visual Template Editor in Blazor WASM | A high-level tour of the decisions, architecture, and shared systems behind the editor. |
| Building Consistent Blazor UX with Source Generators | Turning typed C# models into consistent property controls, previews, and rendered blocks with source generators. |
| Undo, Redo, and Live Preview - No Diffs Needed | Compact property addresses, generated tracking facades, transactions, undo/redo, and preview state. |
| Why Our Blazor App Still Uses TypeScript | A practical split between Blazor-owned application state and TypeScript-owned browser behavior. |
| Beyond Text Replacement: Building Typed Template Variables | Keeping Handlebars for content while extending variables to colors, sizes, images, and other typed properties. |
| Building a System to Generate Complex CSS | Generating deterministic CSS for nested blocks, panels, previews, and both browser and server renderers. |
| Simplifying Complexity: MemoryPack to the Rescue | MemoryPack storage, read-time schema repair, and typed MagicOnion communication across browser and server. |
| Productionizing Our Blazor WebAssembly Editor | Runtime performance, trimming, loading experience, and browser-backed testing for a production Blazor WASM editor. |
