hear what the community is talking about

Community Blogs

We’ve gathered blog posts from across the internet to highlight the many voices that make up our community. Powered by Umbraco, this space brings together diverse stories, ideas, and perspectives in one easy‑to‑explore hub. Dive in and discover what the community is creating, sharing, and talking about.

Want to add your future blog posts to the list? Submit it here!

Umbraco

Building with Claude and Umbraco: Setting Up MCP and Creating Content with AI

I'll be honest, when Jason first mentioned setting up an MCP server to connect Claude to our Umbraco dev environment, I assumed it was going to be a difficult dev-heavy project that would take the day at least, and be a bit past my coding comprehension ability as a Front End Developer. I even made a new local branch assuming I was going to have to write and commit code. Nope! It wasn't like that at all. And I'm still a little surprised about it. It was just setting up a config file in Claude and setting up API users in Umbraco. If you're an Umbraco developer who's been curious about AI-assisted content workflows but figured the setup overhead wasn't worth it, this post is for you. I'm going to walk through exactly what I did, what tripped me up (spoiler: one small thing), and what became possible on the other side of it. Part 1: What Is the Umbraco MCP Server, Anyway? MCP stands for Model Context Protocol. It's an open standard that lets AI assistants like Claude connect directly to external tools and services, not just chat about them, but actually interact with them. The Umbraco MCP server is an implementation of that standard for Umbraco. Once it's set up and connected, Claude can read and write content in your Umbraco instance through the Management API. We're talking creating pages, updating properties, working with block components, the same things you'd do in the back office, but driven by Claude. It's actually pretty amazing to see it work. Part 2: Setting Up the MCP Server Before anything else, a quick note on prerequisites. You'll need an Umbraco site to connect to (we're using our dev environment), and Claude Desktop or Cowork installed on your machine. That's really it on the "before you start" front, which was the first pleasant surprise. The MCP server itself is configured through a JSON config file in Claude. Go to your Claude settings, then Developer, and click the "Edit Config" button to bring up the claude_desktop_config.json file in your favorite editor. Note: Close Claude completely (including killing it in the taskbar) while working on the config file. If Claude is running, it will overwrite your changes when it closes. Note: Make sure you're on the latest version of Node before you start. Drop this into your config file, change the name as you like, change the BASE_URL to your site, and we'll cover the CLIENT_ID and SECRET in the next steps. "mcpServers": { "YOUR-umbraco-dev-mcp-server": { "command": "npx", "args": [ "@umbraco-cms/mcp-dev@latest" ], "env": { "UMBRACO_CLIENT_ID": "umbraco-back-office-api-user", "UMBRACO_CLIENT_SECRET": "12345-GUID-OR-PASSWORD-6789", "UMBRACO_BASE_URL": "https://your-site.io", "UMBRACO_INCLUDE_TOOL_COLLECTIONS": "document,media,document-type,data-type" } } } Part 3: Creating an API User in the Umbraco Back Office This is the part I was most nervous about. "API user" sounds like something that involves a terminal and a lot of Googling. It does not. It's just a few clicks in the Umbraco back office and I was genuinely a little embarrassed by how straightforward it was. Head into your Umbraco back office and navigate to the Users section. We're going to create both a new User Group and a new API user. First, the User Group. Create one and call it MCP-User-Developer and give it as much control as a developer would. If you ever wanted another group called MCP-User-Editor with less control, you can do that too. For now, add Content, Media and Settings sections and turn on everything you'd like it to have access to. Next, go to Users and create a new API User. Name it something like "something-mcp-developer" and make it a member of the MCP-User-Developer group we just made. Add an email address for its username and check on access to start nodes (or give it full access). Now the key step: click the Client Credentials button to add the key we'll need in the Claude config file. The ID will have a prefix of "umbraco-back-office" and then you name it whatever you want. In my example I went with "api-user", so the full ID becomes "umbraco-back-office-api-user". For the Secret, generate a GUID or secure password however you like. Note: Once you click Create, the password won't be visible again. Save it somewhere before moving on! Now we have all the info for the config file. Go back and add "umbraco-back-office-api-user" to the UMBRACO_CLIENT_ID and your password to the UMBRACO_CLIENT_SECRET. Part 4: Connecting Claude to Your Umbraco Site At this point you've got the config file set up and your API user created. The last step is getting Claude to actually recognize the connection. Save your claude_desktop_config.json file and restart Claude. Go to Settings, then Developer again, and you should see your MCP server listed there and running. I was stuck here for a bit because I was not on the latest version of Node, and also because I had the UMBRACO_CLIENT_ID wrong. Woops! So make sure your config has all the correct info. But if everything checks out you should see a "running" label next to your server, and you're now connected! A quick way to confirm everything is working is to just ask Claude something simple about your site. Something like "list the pages under the home node" or "what document types are available?" If Claude comes back with real data from your Umbraco instance, you're in business. As soon as it gave me the ID of the home node of our dev site, I knew things were about to get real. Part 5: Creating Content with Claude This is the fun part. Once Claude is connected to your Umbraco instance, you can start asking it to do real content work. Not just drafting copy in the chat window, but actually creating and populating pages inside Umbraco. I started simple, asking Claude to create a Basic Page under the home node with a title and some body text. It did it. First try. I went and checked the back office and there it was, sitting as a draft, ready to review. From there I started pushing it further. Our Basic Page document type has a bunch of block components available, things like Feature Boxes, Statistics, Testimonials, FAQ sections, Numbered Steps, and a Two-Column Comparison block. I described what I wanted the page to be about and asked Claude to use as many relevant components as made sense for the content. The results were genuinely impressive, well-structured pages with appropriate blocks chosen for the content, not just a wall of rich text. A few tips from what I've learned so far: Give Claude context about your document types. The more it knows about your block components and what fields they have, the better the output. Keep pages as drafts first. Claude creates content as drafts by default, which is exactly what you want. Review before publishing. Be specific about tone and content. Claude will make reasonable choices on its own, but the more direction you give it, the closer the first draft will be to what you actually want. Watch out for placeholder content. Claude may generate fictional quotes or statistics as placeholders. Always review before anything goes live. One small thing to be aware of: on our setup, pages created via the MCP server sometimes needed the template set manually in the back office before they'd render on the front end. It's a quick fix, just one more thing to check on your first run through. Part 6: Real-World Use Cases So what's this actually good for in practice? After spending some time with it, here are the use cases that have stood out most. Spinning up draft pages quickly. If you've got a list of pages that need to exist, Claude can create and populate them fast. What might take an editor an afternoon of copying, pasting and formatting can happen in minutes. The drafts still need a human review pass, but you're starting from something solid rather than a blank page. Consistent page structure across a site. When Claude is working from the same document type and block components every time, the output is naturally consistent. Every page gets a hero, a logical block order, appropriate use of the available components. It's a lot harder to end up with that one orphaned page that's formatted completely differently from everything else. Exploring content strategy. This one surprised me. Being able to say "create a page making the case for upgrading Umbraco and use whichever block components make sense" and get back a fully structured draft is genuinely useful for figuring out what you actually want to say. It's a fast way to go from a topic to a first draft you can react to. Auditing and rewriting existing content. Claude can read your existing pages too, which means you can ask it to review copy, flag inconsistencies, or suggest rewrites across multiple pages without touching the back office yourself. Like I said, I knew things were about to get real. We're still figuring out the best workflows ourselves at ProWorks, but it's already changing how we think about content production. I'm excited to see where it goes from here. If you have questions or want to share what you've built with it, feel free to reach out. Alan Ballard, Front End Developer, ProWorks

by Proworks

I'm an Umbraco MVP for the fifth year running!

Every year, Umbraco recognises the people who have gone above and beyond with their contributions to Umbraco projects and community, and I am pleased to announce that I am honoured to be renewed as an Umbraco MVP for the fifth consecutive year. To celebrate this achievement, I also wrote an article which you can read here. Thank you, Umbraco, and the Umbraco community, for their fantastic support in making this happen once more! #h5yr

by Nurhak Kaya

Validating an Umbraco 13 to 17 Upgrade with a Sitemap Audit

How I used a small .NET console tool to crawl a sitemap, compare Umbraco 13 and 17 environments, and quickly find broken pages after an upgrade....

by Johan Reitsma

You Never Know What'll Stick

On unconventional career paths, accidental dev tools, and great people. There

by Briony McKenzie

Boost Your Workflow with Umbraco 17: Developer Productivity Hacks

As developers, we’re always searching for ways to get our work done faster. New frameworks roll out and promise to make us blazing fast, AI tools keep booming with talk about writing half our code, and if you’ve ever sat through a developer conference lately, you know there’s always a session on “How to be More Productive.” But after working with Umbraco since version 4, I have to be honest: those massive productivity leaps don’t really come from the shiny, new, revolutionary tools. It all comes down to friction. And not the dramatic, show-stopping kind. I mean those hundreds of tiny speed bumps scattered all over your day. A few extra clicks in the Umbraco backoffice. Rebuilding the same component again and again. Always double-checking a property alias. Waiting for your solution to spin up. Poring over a monster Razor file, desperately trying to recall where you put that chunk of markup. None of these feel like deal-breakers on their own, but stacked together? Suddenly, you’re delivering features at a crawl. What’s won me over with the latest Umbraco versions, especially Umbraco 17, is just how much more aligned the platform is with standard .NET development. It feels less like you’re wrestling with a quirky CMS and more like you’re just building .NET applications. There’s a real payoff: less fighting the system means more time solving real business problems.Over the years I’ve picked up quite a few habits that speed up Umbraco development and make it less painful—some are tied directly to Umbraco 17, while others are just hard-earned lessons from a decade of building websites. Here’s what’s helped the most for me.

by Dave Jonker

Extending Tiptap in Umbraco – a real world list example

If you've spent any time migrating editors or building rich-text features, you'll know Tiptap looks great on paper but sometimes feels a little... minimal compared to what TinyMCE shipped out of the box. At ilionx we've been moving a few large sites to Umbraco 17 and bumped into the same feedback: list style options. Nothing dramatic, just the ability to pick Roman numerals, different bullet shapes, that sort of thing, but enough to make editors ask for the old behavior back. So we wrote a small TypeScript extension that adds back list-style variants. It gets built to App_Plugins with Vite, plugs into the UmbTiptapExtensionApiBase and wires up extra toolbar menu items. The actual extension is tiny, it just adds a listStyleType attribute to the list nodes and renders it as inline CSS, but the surrounding bits (icons, toolbar menus, wiring) make the UX feel polished. Tiptap has solid docs on extending and creating extensions, and Umbraco documents how to register a Tiptap extension, but stitching everything together took a little trial and error. Below is the extension configuration we used. It looks like a lot for a simple feature, but a fair chunk of the files are just icons and small glue code. Let’s start with the actual extension definitions in Umbraco-package.json, where we define 4 extensions: "extensions": [ { "type": "icons", "alias": "My.Icons.TiptapListStyle", "js": "/App_Plugins/My.TiptapListStyle/list-style-icons.js" }, { "type": "tiptapExtension", "alias": "My.Tiptap.ListStyle", "api": "/App_Plugins/My.TiptapListStyle/list-style.tiptap-api.js", "meta": { "icon": "icon-ordered-list", "label": "List style type", "group": "#tiptap_extGroup_formatting" } }, { "type": "tiptapToolbarExtension", "kind": "menu", "alias": "My.Tiptap.Toolbar.OrderedList", "overwrites": "Umb.Tiptap.Toolbar.OrderedList", "api": "/App_Plugins/ My.TiptapListStyle/ordered-list-menu.tiptap-toolbar-api.js", "forExtensions": ["My.Tiptap.ListStyle", " My.Tiptap.OrderedList"], "items": [ { "label": "Numbered list", "data": null, "appearance": { "icon": "icon-ordered-list" } }, { "label": "Numbered list: i, ii, iii", "data": "lower-roman", "appearance": { "icon": "icon-list-style-lower-roman" } }, // add other style definitions here ] }, { "type": "tiptapToolbarExtension", "kind": "menu", "alias": " My.Tiptap.Toolbar.BulletList", "overwrites": "Umb.Tiptap.Toolbar.BulletList", "api": "/App_Plugins/My.TiptapListStyle/bullet-list-menu.tiptap-toolbar-api.js", "forExtensions": ["My.Tiptap.ListStyle", "Umb.Tiptap.BulletList"], "items": [ { "label": "Bullet list", "data": null, "appearance": { "icon": "icon-bulleted-list" } }, { "label": "Bullet list: circle", "data": "circle", "appearance": { "icon": "icon-list-style-bullet-circle" } }, // add other style definitions here ] } ] Here’s what we register: an icon set (a bunch of tiny SVGs showing what each list style looks like) one Tiptap extension which carries the list-style attribute two toolbar extensions that replace the ordered/bulleted list buttons with menus that expose the variants. The icon set The icons are trivial: I asked an AI to generate SVGs for different list markers and put each one in a tiny TypeScript file that exports the SVG string. /** Ordered list with lower-roman markers (i, ii, iii). */ export default `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.75"><text x="1" y="7.5" font-size="4.5" fill="currentColor" stroke="none" font-family="serif" font-style="italic">i.</text><path d="M9 6h12"/><text x="1" y="13.5" font-size="4.5" fill="currentColor" stroke="none" font-family="serif" font-style="italic">ii.</text><path d="M9 12h12"/><text x="1" y="19.5" font-size="4.5" fill="currentColor" stroke="none" font-family="serif" font-style="italic">iii.</text><path d="M9 18h12"/></svg>`; And a small registry file maps the icon names to those files: export default [ { name: 'icon-list-style-lower-roman', path: () => import('./icons/svg-ordered-list-lower-roman.js'), }, //... ]; These images are purely to make the toolbar menus readable — a little UX polish so editors instantly recognise the option they’re picking. The Tiptap extension The extension itself is surprisingly compact. It adds a listStyleType attribute to the built-in orderedList and bulletList nodes and serialises it as inline list-style-type style so the HTML stays portable and renders the same in the backoffice and on the front end. import { Extension, UmbTiptapExtensionApiBase } from '@umbraco-cms/backoffice/tiptap'; /** * Extends the built-in orderedList and bulletList nodes with a `listStyleType` * attribute that maps to the CSS `list-style-type` property. * * The attribute is serialised as an inline style so the stored HTML is portable * and renders correctly both in the backoffice editor and on the front end. */ const ListStyleExtension = Extension.create({ name: 'listStyle', addGlobalAttributes() { return [ { types: ['orderedList', 'bulletList'], attributes: { listStyleType: { default: null, parseHTML: (element) => element.style.listStyleType || null, renderHTML: (attributes) => { if (!attributes['listStyleType']) return {}; return { style: `list-style-type: ${attributes['listStyleType']}` }; }, }, }, }, ]; }, }); export default class ListStyleTiptapExtensionApi extends UmbTiptapExtensionApiBase { constructor(...args: ConstructorParameters<typeof UmbTiptapExtensionApiBase>) { super(...args); this.getTiptapExtensions = () => [ListStyleExtension]; } } That’s it, the extension maps an attribute to CSS and makes sure works in both front- and backoffice. Nothing scary. The toolbar extensions Because ordered and unordered lists are separate toolbar buttons, we overwrite each one with a menu that exposes the style variants. To avoid duplicating logic we put the toolbar behaviour into a single helper and import it from the two small extension files. Each toolbar menu has to do a few things: create a list if the selection isn’t in one switch between ordered and bullet lists toggle or set a specific list-style-type and reflect the active state in the menu UI. That last bit is where the logic concentrates, the UI needs to show which style is active and allow toggling back to the default behaviour. The per-list-type extension files look like this: import { createListStyleMenuToolbarApi } from './list-style-menu-toolbar-api.js'; export default createListStyleMenuToolbarApi('bulletList'); And the shared toolbar helper contains the actual implementation and state logic: import type { Editor } from '@umbraco-cms/backoffice/tiptap'; import { UmbTiptapToolbarElementApiBase } from '@umbraco-cms/backoffice/tiptap'; type ListStyleMenuItem = { data?: string | null }; function getStyleType(item?: ListStyleMenuItem): string | null | undefined { return item?.data; } function applyListStyle( editor: Editor, listType: 'orderedList' | 'bulletList', styleType: string, ): void { if (editor.isActive(listType)) { const currentStyle = editor.getAttributes(listType)['listStyleType'] as string | null; const newStyle = currentStyle === styleType ? null : styleType; editor.chain().focus().updateAttributes(listType, { listStyleType: newStyle }).run(); return; } if (listType === 'orderedList') { editor .chain() .focus() .toggleOrderedList() .updateAttributes('orderedList', { listStyleType: styleType }) .run(); } else { editor .chain() .focus() .toggleBulletList() .updateAttributes('bulletList', { listStyleType: styleType }) .run(); } } function applyDefaultList(editor: Editor, listType: 'orderedList' | 'bulletList'): void { if (editor.isActive(listType)) { const currentStyle = editor.getAttributes(listType)['listStyleType'] as string | null; if (currentStyle) { editor.chain().focus().updateAttributes(listType, { listStyleType: null }).run(); return; } if (listType === 'orderedList') { editor.chain().focus().toggleOrderedList().run(); } else { editor.chain().focus().toggleBulletList().run(); } return; } if (listType === 'orderedList') { editor.chain().focus().toggleOrderedList().run(); } else { editor.chain().focus().toggleBulletList().run(); } } /** * Toolbar menu API for ordered/bullet list buttons with list-style-type variants. */ export function createListStyleMenuToolbarApi(listType: 'orderedList' | 'bulletList') { return class extends UmbTiptapToolbarElementApiBase { override isActive(editor?: Editor, item?: ListStyleMenuItem): boolean { if (!editor) return false; const styleType = getStyleType(item); if (styleType === undefined) { return editor.isActive(listType); } if (styleType === null) { const currentStyle = editor.getAttributes(listType)['listStyleType'] as string | null; return editor.isActive(listType) && currentStyle === null; } return editor.isActive(listType, { listStyleType: styleType }); } override execute(editor?: Editor, item?: ListStyleMenuItem): void { if (!editor) return; const styleType = getStyleType(item); if (styleType === undefined || styleType === null) { applyDefaultList(editor, listType); return; } applyListStyle(editor, listType, styleType); } }; } TODO / caveats I’m still getting comfortable with TypeScript, so this isn't a 100%-polished extension. One known quirk: clicking a custom style button after setting a style will clear the style but not fully remove the list node the way the stock ordered/bullet button does. It's a small UX edge-case and likely fixable by tweaking the toggle logic, but I haven't had time for it yet. If you want to copy this approach, the important bits are: expose a listStyleType attribute on the list nodes, serialize it in a way that works in both editor and front end (inline style is the simplest) replace the toolbar buttons with menus that set/update that attribute. Hopefully this saves you a few hours of experimentation. The overall idea is small, but the payoff for editors is surprisingly nice.

by Bernadet Goey

My First CodeGarden: AI, Umbraco 17 and 18, and Why I

This was my first time travelling to Copenhagen, and my first ever CodeGarden, and what a way to start. I went over with Luca Colella and Kat Dixon and we had a brilliant couple of days.

by Raghavendra Murthy

What Codegarden Inspired Me to Build Next

What I learned building an AI-assisted pipeline that maps existing sites into reusable Umbraco components.

by Mark McDonald

Codegarden 2026: Ubuntu

"I am because we are"

by Mitchell Nortje

Extending Umbraco's Extensions: A Codegarden Workshop You Can Try Yourself

Clone and try our four-hour Codegarden workshop on extending Umbraco's extensions: an escape room of coding quests across Engage, Workflow and Deploy.

by Corné Hoskam

It's Coffee Time!

by Owain Williams

The most interesting thing at CodeGarden wasn

There were plenty of shiny things to look at this year. New content types, a workflow engine, hosted servers you can spin up in minutes.

by Adam Shallcross