I recently built and published my first Umbraco extension: ThtaAI, an open-source package that adds AI-assisted content generation to the Umbraco 17 backoffice. What started as a fairly simple idea, could I put a "generate with AI" button next to an Umbraco field? Gradually turned into something considerably bigger. By the end, I had AI-assisted text fields, rich text generation, image search, configurable language models and, eventually, a tool capable of looking at an Umbraco site's content structure and generating an entire unpublished page from a natural-language prompt.
As this was my first proper Umbraco extension, there was quite a lot of learning along the way. Looking back through the commit history, you can actually see the project changing as I encountered each new problem. So rather than just documenting the finished package, I thought it would be useful to write about how I got there.
Starting small
The first version of the project appeared in May 2026. I deliberately started with something relatively contained: extending individual content fields rather than attempting to generate entire pages. The basic idea was that an editor could open a field in the Umbraco backoffice, ask an LLM to generate some content, refine the result conversationally and then insert it into the property.
That became the basis of the AI Textstring and AI Textarea property editors. The important thing here was keeping AI generation separate from publishing. The model suggests content; the editor remains in control of what actually ends up on the website. That principle has stayed with the package as it has grown.
Extending the Rich Text Editor
One of the first things I wanted after getting basic text generation working was integration with Umbraco's Rich Text Editor. Creating a completely separate AI editor would have worked, but it wouldn't have been particularly pleasant to use. If somebody is already writing content, I don't want them constantly jumping between different screens just to generate a paragraph. So the next experiment was a Rich Text Editor extension.
Instead of replacing the editing experience, ThtaAI adds an AI action to the editor toolbar. You can place the cursor where you want the content, generate it and insert the result directly into the document. This was one of my first useful lessons from building an Umbraco extension:
the best extension isn't necessarily a new interface, sometimes it is something that fits naturally into the interface editors already use.
Adding images
Once text generation was working, images were an obvious next problem. The package gained an AI Image property editor in early June.
I took a slightly different approach here. Rather than generating images directly with the language model, the editor can use a prompt to retrieve several possible images from Pixabay and let the editor choose the most appropriate result.
Again, the human stays in the loop. That started becoming a recurring design decision throughout the project: AI should accelerate the content editor's work rather than silently make publishing decisions for them.
Making the model configurable
Another early assumption I had to get rid of was that there would be one model. During development I'd been running models locally using Ollama. That's useful because prompts and content can remain within your own infrastructure, but different models have very different capabilities. So model selection became configurable rather than being baked into the extension.
ThtaAI now talks to a configurable LLM endpoint and model, meaning a local Ollama instance can be used, but the same approach can also work with other compatible APIs. That flexibility became particularly important when I moved on to the feature that caused most of the interesting problems.
Could AI generate an entire Umbraco page?
Generating a string is relatively straightforward. Generating an Umbraco page isn't. An Umbraco site already has a content model. There are document types, properties, Block Lists, Block Grids, element types, nested blocks and editor-specific expectations about what valid data looks like. So asking an LLM:
"Create me a landing page about our new service"
is only half of the problem. The model also needs to understand what a valid landing page on this particular Umbraco site looks like.
In June I added a dedicated AI Generation section to the backoffice and started experimenting with exactly that. The idea was to inspect the site's available content types and turn them into a schema the model could understand. Then the model could generate content against the site's actual structure rather than inventing its own. This is where things got considerably more difficult.
The point where prompt engineering stopped helping
There's a commit in the repository that sums up this stage of the project rather nicely:
"Got a point where no amount of prompt engineering is going to get the AI to generate the content in a way that is useful. I need a more powerful llm."
I think that's probably my favourite commit message in the project. It also describes an important lesson. When working with LLMs, it is very tempting to assume every bad result can be fixed by changing the prompt.
Add another instruction.
Give it another example.
Tell it more emphatically to return JSON.
Eventually, though, you have to recognise that you're asking the model to reason about a complicated structure and there are limits to what prompt tweaking can solve. For whole-page generation I needed a combination of a capable enough model and a better architecture around it. The application needed to do more of the deterministic work itself.
Moving schema processing to the backend
The next major change was moving the cleaning and preparation of the Umbraco schema into the backend. Rather than sending a large amount of raw Umbraco information to the model and expecting it to figure everything out, ThtaAI started transforming that information into something specifically designed for the generation task. That separation helped considerably. It also reinforced something I'd apply to future AI integrations:
don't ask the LLM to solve problems normal code can solve reliably.
Use code to discover the site's content types.
Use code to determine which properties and blocks are valid.
Use code to construct a constrained representation.
Then give the model the part of the problem where natural-language reasoning is actually useful.
Finally: a valid page schema
A few days later, another commit appeared:
"Finally llm is returning valid page schema"
"Finally" was doing quite a lot of work in that sentence. Getting valid JSON wasn't really the victory. The important part was getting output that corresponded to structures Umbraco could actually understand. But even then I wasn't finished. The same commit contains the next problem:
"need to revisit dropdown options and mapping to umbraco publishing"
This became another recurring theme of the project. Getting the AI to produce something that looks right is different from producing something that can actually be persisted correctly in a CMS.
A dropdown isn't just arbitrary text.
A block isn't just an object.
An Umbraco property editor has expectations about its stored value.
So the generation layer needed more information about individual field types rather than treating every property as interchangeable.
Nested blocks and field types
The next iteration added nested blocks and field-type information to the schema sent to the model. This made a significant difference because the LLM could make decisions based on what a property actually represented. It could understand not only that a document type contained a property, but also what kind of content belonged there and what blocks were valid inside other blocks. That meant generation could start reflecting the actual content architecture of the site. This is where the feature began feeling less like:
"AI writes some JSON."
and more like:
"AI understands enough about this Umbraco site to construct content for it."
From schema generation to a real Umbraco page
The next hurdle was mapping that generated structure back into Umbraco. Changes to the content mapping service eventually got the project to the point where AI Page Generation could create full pages.
The current process is roughly:
Inspect the site's document types and available block structures.
Convert those into a schema suitable for the model.
Ask the editor what they want to create.
Generate content conforming to that schema.
Map the generated values back to their corresponding Umbraco property editors.
Create the page beneath the selected parent.
And there's an important final detail:
the generated page is left unpublished.
AI can create the first draft, but an editor reviews, changes and publishes it using the normal Umbraco workflow. For me, that's a much more useful application of generative AI in a CMS than attempting to automate the editorial process completely.
UX matters too
Once the difficult technical parts started working, another problem became obvious. The interface had been designed around developing the feature rather than using the feature. So several later commits are simply about redesigning the schema and AI Page Generation screens to make them more understandable. That's another lesson I'll take from building my first extension.
Getting an extension technically working inside Umbraco is only the first part.
If it's a backoffice extension, content editors are your users. They shouldn't need to understand document type schemas, JSON, LLM context windows or how the generation pipeline works. They should be able to describe what they want and understand what the extension is going to do.
Turning a project into an actual Umbraco package
Once everything worked locally, I discovered another whole category of work: turning it into something other people could install. The final run of commits tells that story quite clearly:
preparing the .csproj and README for builds;
adding licence and GitHub metadata;
setting up GitHub and automated NuGet publishing;
adding the package icon;
creating umbraco-marketplace.json;
adding Marketplace screenshots;
fixing the screenshot URLs when they inevitably weren't quite right;
and then several package-version bumps while getting the published package into shape.
There's something satisfying about seeing:
dotnet add package thtaai
after spending weeks with the project existing purely as source code.
Packaging isn't the glamorous part of extension development, but making something genuinely installable, documented and discoverable is what turns an internal experiment into something the wider Umbraco community can actually use.
What ThtaAI does today
The extension now includes:
AI Textstring and Textarea editors
Generate and refine copy conversationally before inserting it into a property.
Rich Text Editor integration
Generate content directly from the RTE toolbar and insert it at the current cursor position.
AI Image editor
Search for image variations based on a natural-language description and select the best result.
AI Page Generation
Inspect the site's document types and block configuration and generate a complete page that follows the existing content architecture. It currently targets Umbraco 17 and can use a local LLM through something such as Ollama or another compatible LLM API.
What I learned from writing my first Umbraco extension
Probably the biggest thing I learned is that there is a huge difference between demonstrating an AI feature and integrating one properly into a CMS.
Making an LLM generate text is easy.
Making that generation understand your CMS structure, respect its content model, map values into the correct editors, handle nested content and fit into an editorial workflow is where the interesting engineering starts.
I also learned not to make the LLM responsible for everything.
The eventual architecture became better as more deterministic work moved back into C# and the model was given a smaller, better-defined problem.
And finally: build the smallest useful extension first.
If I'd started this project with "generate an entire Umbraco page including nested Block Grids", I suspect I wouldn't have got very far. Starting with one field meant I could learn how Umbraco's extension model worked. The RTE introduced another part of the backoffice. Images added another content type. Only then did whole-page generation become a sensible problem to tackle.
The commit history is messy in places, there are multiple commits containing the word "finally", and there were more package version bumps than I expected. But that's also why I've left the project open source.
If you're thinking about building your first Umbraco extension, particularly something involving AI or the new backoffice, hopefully there is something useful in there.
Try it or have a look at the source
GitHub: source code and full commit history
https://github.com/dwhalley15/thtaai
The repository is open source under the MIT licence, so feel free to dig through the implementation, look through the history, raise an issue, fork it or contribute.
Umbraco Marketplace
https://marketplace.umbraco.com/package/thtaai
NuGet
https://www.nuget.org/packages/ThtaAi
Or install it directly:
dotnet add package thtaai
This was my first attempt at taking an Umbraco extension all the way from an idea, through the inevitable "why isn't this mapping correctly?" stage, to an open-source package on NuGet and the Umbraco Marketplace.
I'm fairly sure it won't be my last.