Why pages are component trees, not HTML blobs
If an LLM returns HTML you cannot review it and cannot constrain it. Validated component trees are what make AI editing survivable in a checkout.
There are two ways to let a language model edit a web page, and the choice you make in week one decides what the product can safely do in year one.
Option A: the model returns HTML. Prompt in, markup out, render it. It is a two-hour integration and it demos beautifully.
Option B: the model edits a structured tree of components you defined. It cannot emit markup at all. It can only rearrange, add, remove, and reconfigure things from a catalog you control.
Autonnel took option B. Pages are Puck component trees: HeroBanner, ProductSelector, OrderSummary, PaymentForm, and so on, each with typed props. This post is about why, and it is not mainly an aesthetic argument.
Option A stops being fun the moment money is involved
An HTML blob is opaque to every system you would want to protect yourself with.
You cannot review it. When the model rewrites a section and returns 400 lines of markup, the diff against the previous 380 lines is noise. Class names shifted, whitespace moved, the DOM order changed in three places. Somewhere in there it also changed a link. You will not find it by reading, and after the third time you stop trying, which means you have a review step that exists on the org chart and not in reality.
You cannot constrain it. Nothing in an HTML string prevents a <script> tag, an off-domain form action, an iframe, or a rewritten checkout button. You end up writing a sanitiser: an allowlist of tags, attributes and URL schemes, applied to untrusted generated markup, on the page that takes credit cards. That is a security surface you volunteered for, and sanitisers are famously never finished.
You cannot reuse it. The blob is not a thing your editor understands. Your drag-and-drop builder cannot open it, your template system cannot parameterise it, and your A/B machinery cannot swap one section of it. So the AI path forks away from the manual path and you maintain two page representations forever.
None of these bite in the demo. All of them bite in production, and the checkout page is the worst possible place to discover them.
What option B buys
The page is a tree of typed component instances. The model’s job is not to write a page; it is to produce a new tree using components that already exist.
Validation is structural, not textual. Every agent run is checked against the component catalog. Invalid component names or prop values cause the run to be rejected outright with an error, and there is no automatic retry. The model cannot invent a RawHtml component because there is not one. It cannot smuggle a script tag into checkout because there is nowhere in the schema to put a script tag. This is a stronger guarantee than a sanitiser, and unlike a sanitiser it is exhaustive by construction: the set of things that can appear on the page is exactly the set of components you shipped.
Changes are legible. A model rewriting a hero produces a change to HeroBanner.headline and HeroBanner.ctaHref. Whatever you build on top of that (a diff view, an audit log, an approval queue) has something meaningful to show. With a blob, none of those are buildable in a useful form.
One representation, three authoring paths. Drag-and-drop, template instantiation and AI editing all produce the same tree. They compose: generate with a prompt, nudge one prop by hand, save the result as a template. That is only possible because they are not three formats.
The interactive parts stay ours. PaymentForm, AddressForm, OrderSummary and CouponInput are real components that hydrate as islands, wired to the order state. The model arranges them; it never reimplements them. An LLM has no business generating the markup of a payment form, and under this design it structurally cannot.
The costs, because there are real ones
You have to build a component catalog, and it is the actual product work. Autonnel ships layout, product, checkout, social-proof and content components with typed props. Every one is code someone wrote, documented, and now maintains. Option A needs none of that. If your goal is a landing-page toy, option A is genuinely the correct engineering decision and I would not argue with it.
The model can only express what the catalog can express. Ask for a layout nothing in the catalog supports and you get an approximation, not the thing you asked for. The ceiling on output is the catalog, not the model. In practice this reads as the system being tastefully consistent, which is usually what you want on a store, and occasionally as it being stubborn.
Catalog changes are migrations. Renaming a prop means touching stored pages. Blobs never have this problem, because blobs never have any schema problems, which is precisely the trade.
Every agent call ships the catalog. The current layout plus the full component catalog go to your LLM on each run. That is real token cost and it grows with the catalog. Worth checking your provider’s pricing before you loop on one page all afternoon.
What we still owe this design
Storing pages as trees makes a diff view possible. We have not built one. Today the in-editor Agent panel edits in place and autosaves the draft, so if you want a checkpoint before experimenting you duplicate the page first. That is a worse workflow than the data model deserves, and it is the gap I am least happy about.
What saves it in the meantime is that draft and published are separate states. Autosave writes drafts. Making a page live is a distinct action a human takes. Nothing an agent does reaches a customer without someone pressing publish.
The general version
If you are adding AI editing to anything, the question that matters is not which model or which prompt. It is:
What is the smallest set of things the model is allowed to produce?
Answer that first, encode it as a schema, and validate every generation against it. The narrower the answer, the more you can safely automate, because everything downstream (review, audit, rollback, permissions) needs something structured to grab hold of.
“The model returns text and we render it” is the widest possible answer. It is fine when the output is a blog draft. It is not fine when the output is the page that takes the card.
More on the editor and the component catalog in the Puck editor docs, the AI path in AI page generation, and how agents drive the same tree over MCP in the MCP server docs.
All of it is Apache-2.0 at github.com/autonnel/autonnel, free to self-host, and the open-source ClickFunnels alternative page covers what it replaces and what it deliberately does not do.