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? Contact us here and let us know!

Umbraco

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

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

Codegarden 2026 and the next chapter for Umbraco | Shout Digital

AI, automation, community - Umbraco MVP Ian Grieve talks about his key takeaways from Copenhagen.

by Ian Grieve

Copenhagen, Codegarden, and My First Year as an Umbraco MVP

My first year as an Umbraco MVP: receiving the award at Codegarden 2026 in Copenhagen, the MVP Summit, the sessions, the people, and the moments between.

by Nathaniel Nunes

Tilbake fra København og Umbraco Codegarden 2026! 🇩🇰

Noen dager fylt med faglig påfyll, gode samtaler og sterke relasjoner — det er alltid noe eget med Codegarden. Umbraco-communityet er ikke en kult, men… det er kanskje det nærmeste vi kommer …

by Tom Erik Rozmara Frydenlund