Welcome to the Umbraco Community!

The Umbraco Community is at the heart of the Umbraco ecosystem. It’s built on people coming together to share ideas, help each other, and make Umbraco better every day.

Come see how the Community stays connected, what makes it such a great place to be, and how you can join in and get involved. We always have room for more people to join!

MVP Summit audience.jpg 54610954933_1fd227623f_o (1).jpg Panel speaking in mics at umbraco event Person looking at laptop 52993341606_3b3a6a6991_o.jpg 2025 Group picture 1740139582170.jpg IMG_4217.jpg IMG20251030182101.jpg 2025 Community Teams day at Umbraco HQ DSC04687.jpg DSC05663.jpg MVP getting high five on stage MVPs speaking at MVP Summit 2 MVP waving at MVP Summit MVPs showing off awards IMG20251106141733.jpg 20251204_173423.jpg b6f67a24-17ea-4a21-bc55-65eca97d86ac.jpeg Codegarden 2023 Business Summit - people talking.jpg People high five'ing at Codegarden.jpg Boat ride 2025 Codegarden runners running

Get involved in the community

There's so many ways to get involved in the Umbraco community. 

A person looking into his laptop surrounded by people and light

New here? Welcome! We have a complete newcomer's guide to Umbraco and the Umbraco Community waiting for you.

A woman concentrating and looking down

No matter your interests, skills and time availability, there’s a way for you to participate in the Umbraco community. 

Two people high five'ing

Umbraco Meetups are a great way to engage with the Umbraco community and meet other like-minded people.

See what's happening

Join a meetup or an event near you - see all upcoming Umbraco events here

Blog posts from the Umbraco Community

Explore the latest blog posts from our community. Want to add your future blog posts to the list? Contact us here and let us know!

Building a Cloudflare Integration for Umbraco Automate

When I first started experimenting with Umbraco Automate, I wanted to find a real-world use case instead of creating an integration just for the sake of trying the API. It didn't take long to find one. On several Umbraco projects, we use Cloudflare in front of the website. When content changes, there are situations where we want to invalidate the cached version of the affected page. Traditionally, I would solve this in code: Content published ↓ Notification handler ↓ Determine published URLs ↓ Call Cloudflare API ↓ Purge cache That works. But after looking at Automate, I started wondering: What if the application doesn't need to know anything about Cloudflare at all? Instead, publishing content could simply trigger an automation, with small reusable actions taking care of the individual steps. That idea eventually resulted in three NuGet packages: Umbraco.Community.Automate Umbraco.Community.Automate.Extensions Umbraco.Community.Automate.Cloudflare All currently available as version 17.0.0 for Umbraco 17. This post describes how I got there, some of the problems I encountered, and what I learned about extending Umbraco Automate along the way. Starting with the obvious solution My first idea was simple. Umbraco Automate already provides a Content Published trigger, so I wanted to create a Cloudflare action that could take the published page and purge it from Cloudflare. Something like: Content Published ↓ Purge Cloudflare The Cloudflare API itself makes this fairly straightforward.To purge specific URLs you can send a request to: POST /client/v4/zones/{zoneId}/purge_cache with: { "files": [ "https://www.example.com/some-page/" ] } So initially I thought the Cloudflare action could simply accept the URL of the published page. The Content Published trigger doesn't provide a URL. It exposes information such as: contentKey contentName contentTypeKey contentTypeAlias cultures And actually, that makes sense. A URL isn't necessarily a property of the publish event. A content item doesn't necessarily have one URL This became the first interesting part of the implementation. Consider a multilingual Umbraco website: https://www.example.com/ https://www.example.com/en/ Now add multiple hostnames, which is perfectly valid in an Umbraco installation: https://www.example-a.com/ https://www.example-a.com/en/ https://www.example-b.com/ https://www.example-b.com/en/ For a single content item, asking: What is the URL? is therefore not always the right question. The better question is: What are the published URLs for this content item and culture? That distinction ended up having quite a large influence on the package design. Separating Umbraco from Cloudflare My first instinct was to put the URL resolution inside the Cloudflare action. The action could receive: Content Key Culture and internally: Resolve the Umbraco content. Determine its URLs. Call Cloudflare. Purge those URLs. But that didn't feel right. Resolving published URLs has nothing to do with Cloudflare. The same functionality could be useful when: notifying an external API; updating a search index; sending a webhook; invalidating another CDN; generating a sitemap; or building another Automate workflow. So instead I created a generic action: Get Published Content URLs and moved it into a separate package: Umbraco.Community.Automate.Extensions Cloudflare became its own integration: Umbraco.Community.Automate.Cloudflare This allows Automate to do what it is good at: composing small pieces of functionality. The workflow becomes: Content Published ↓ For Each Culture ↓ Get Published Content URLs ↓ Purge Cloudflare URLs That separation is probably the design decision I'm happiest with. Building Get Published Content URLs The generic action receives a content key and culture. Its settings are roughly: public sealed class GetPublishedContentUrlsSettings { public Guid ContentKey { get; set; } public string Culture { get; set; } = string.Empty; } The output deliberately contains an array: public sealed class GetPublishedContentUrlsOutput { public Guid ContentKey { get; set; } public string Culture { get; set; } = string.Empty; public string[] Urls { get; set; } = []; } Notice that it's Urls, not Url. This is important for installations where a culture is available through multiple domains. Conceptually the action does: Content Key + Culture ↓ Umbraco published content ↓ Primary + alternative URLs ↓ string[] That output can then be consumed by any other Automate action. Lesson learned: be careful with service lifetimes While building the action I ran into another useful lesson. My first implementation injected IPublishedContentQuery directly into the action. That resulted in: Cannot consume scoped service 'Umbraco.Cms.Core.IPublishedContentQuery' from singleton 'GetPublishedContentUrlsAction'. Automate actions are registered as singletons, while several Umbraco services used for published content are scoped. Injecting one directly into a singleton therefore isn't safe. Instead, the action needs to work with the appropriate Umbraco context/service scope when resolving published content. It's an easy mistake to make when building your first Automate extension because the action itself looks very similar to a normal service. The important takeaway: Always consider the lifetime of the services you inject into an Automate action. Cultures introduced another interesting problem The Content Published trigger exposes cultures. Not culture. That means a single publish operation can involve multiple cultures. The obvious workflow is therefore: Content Published ↓ For Each ↓ Get Published Content URLs The collection for the loop is: ${ trigger.cultures } And inside the loop the current culture is: ${ loop.item } Initially I accidentally passed: ${ trigger.cultures } directly into the action's Culture field. That resulted in something like: ["es"] being passed as the culture instead of: es and consequently: Content '<guid>' is not published for culture '["es"]'. The correct pattern is: For Each: ${ trigger.cultures } Get Published Content URLs: Content Key: ${ trigger.contentKey } Culture: ${ loop.item } A small mistake, but a good example of why understanding the difference between a collection binding and the current loop item matters. Then I wanted to purge multiple URLs at once Initially the Cloudflare package had: Purge Cloudflare URL with: public string Url { get; set; } = string.Empty; This worked perfectly when combined with another For Each: Get Published Content URLs ↓ For Each URL ↓ Purge Cloudflare URL But Cloudflare already accepts multiple URLs in one request. So why make one HTTP request per URL? I changed the action to: Purge Cloudflare URLs and the Cloudflare client now accepts a collection: Task PurgeUrlsAsync( string apiToken, string zoneId, IEnumerable<string> urls, CancellationToken cancellationToken = default); The resulting request can contain all URLs: { "files": [ "https://www.example-a.com/en/", "https://www.example-b.com/en/" ] } This makes the workflow much nicer: Get Published Content URLs ↓ Purge Cloudflare URLs At least, that was the idea. And then I learned something interesting about Automate bindings. Binding an array isn't the same as binding a string My first settings model looked perfectly reasonable: public sealed class PurgeUrlsSettings { public string[] Urls { get; set; } = []; } The previous action already returned: string[] Urls so I expected this binding to work: ${ previous.urls } Instead Automate failed before the action was even executed: Failed to resolve model 'community.cloudflare.purgeUrls' to type PurgeUrlsSettings. The JSON value could not be converted to System.String[]. That was confusing at first because the source value really was a string[]. The important detail is how action settings are stored. The binding itself is initially represented as: { "urls": "${ previous.urls }" } That's a string expression. If the property is already declared as string[], JSON deserialization needs to happen before that expression can resolve to its collection value. So: "${ previous.urls }" ↓ Deserialize as string[] ↓ 💥 Looking at For Each provided the answer The interesting thing was that this already worked: Get Published Content URLs ↓ For Each Collection: ${ previous.urls } So Automate clearly could consume my array. That led me into the Automate source code. The built-in For Each doesn't define its collection setting as an array. Instead, conceptually it does this: public string Collection { get; set; } = string.Empty; The setting contains the binding expression, not the resolved collection. The expression is evaluated later at runtime. That same approach works nicely for the Cloudflare action. The setting can remain: public sealed class PurgeUrlsSettings { public string Urls { get; set; } = string.Empty; } and the configured value remains: ${ previous.urls } At runtime, Automate's BindingEvaluator can evaluate the raw expression: var value = _bindingEvaluator.EvaluateRaw( settings.Urls, context.BindingData ?? new Dictionary<string, object?>()); The action can then turn that resolved value into the collection it needs. This was probably the most interesting technical lesson from building the package. Sometimes the type of a configuration property shouldn't represent the type of the eventual runtime value. In this case: Setting string expression ↓ BindingEvaluator ↓ runtime collection ↓ Cloudflare API is a better model. Normalizing the URLs Before sending anything to Cloudflare, the action normalizes the collection: urls = urls .Where(url => !string.IsNullOrWhiteSpace(url)) .Select(url => url.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); Then it verifies that something remains: if (urls.Length == 0) { return ActionResult.Failed( new ArgumentException( "At least one URL is required.")); } and validates the URLs: var invalidUrl = Array.Find( urls, url => !Uri.TryCreate( url, UriKind.Absolute, out _)); if (invalidUrl is not null) { return ActionResult.Failed( new ArgumentException( $"'{invalidUrl}' is not a valid absolute URL.")); } Only after that does the action call Cloudflare. Creating a reusable Cloudflare connection I didn't want API tokens and zone IDs to be configured on every action. Automate has a connection concept for exactly this purpose, so the Cloudflare package provides its own connection type. The connection contains information such as: Account ID Zone ID API Token The API token can also be validated against Cloudflare. Cloudflare provides an endpoint for checking whether a token is valid and active, which makes the connection test much more useful than simply checking whether a value was entered. A successful response looks roughly like: { "result": { "status": "active" }, "success": true } This gives users feedback while configuring the connection instead of waiting for their first automation run to fail. Keep API tokens scoped For the Cloudflare API token, I recommend granting only the permissions required by the workflow. For cache purging, don't use a token with unnecessary account-wide permissions. The connection should know: API Token Zone ID and the action should only perform the operation it was designed for. It's a small detail, but integrations like these make security boundaries very visible. The resulting packages The experiment eventually became three packages. Umbraco.Community.Automate The convenience package. Installing it brings in the complete collection: dotnet add package Umbraco.Community.Automate Umbraco.Community.Automate.Extensions Generic Automate building blocks: dotnet add package Umbraco.Community.Automate.Extensions Currently including: Get Published Content URLs Umbraco.Community.Automate.Cloudflare The Cloudflare integration: dotnet add package Umbraco.Community.Automate.Cloudflare Currently providing the Cloudflare connection and cache purge functionality. The packages are versioned alongside their supported Umbraco major version, so the first stable release is: 17.0.0 for Umbraco 17. Why I like this approach The part I like most isn't actually the Cloudflare API integration. Calling an HTTP API isn't particularly complicated. The interesting part is the separation of responsibilities. Instead of writing: public class ContentPublishedHandler { // Find content // Determine cultures // Determine domains // Resolve URLs // Read Cloudflare configuration // Call Cloudflare // Handle failures } we can create reusable building blocks: Content Published ↓ For Each Culture ↓ Get Published Content URLs ↓ Purge Cloudflare URLs And tomorrow somebody could build: Content Published ↓ Get Published Content URLs ↓ Notify external service or: Content Published ↓ Get Published Content URLs ↓ Update search index without changing Get Published Content URLs. That's where I think Automate becomes really interesting. What I learned A few things stood out while building these packages. Actions should do one thing Get Published Content URLs shouldn't know Cloudflare exists. And Purge Cloudflare URLs shouldn't need to know anything about Umbraco content. The workflow connects them. Model collections explicitly A published content item can have multiple cultures, and one culture can have multiple URLs. Designing around collections from the start avoids assumptions that only work for simple websites. Bindings are runtime values A binding expression such as: ${ previous.urls } is stored as a string but may resolve to something completely different at runtime. Looking at how Automate's own For Each implementation handles this was extremely useful. Watch your DI lifetimes Automate actions and Umbraco scoped services don't necessarily share the same lifetime. Be careful when injecting services such as published-content APIs directly into actions. Composition beats coupling The Cloudflare use case resulted in a generic action that turned out to be useful completely independently of Cloudflare. That's a good sign that the responsibility belongs in its own extension. What's next? There are plenty of directions this could go. Cloudflare supports more than purging individual URLs, so possible future actions include: Purge by cache tag Purge by prefix Purge by hostname Purge everything But I don't want one giant "Cloudflare action" with every possible option. I'd rather keep each operation explicit and let Automate compose them. The generic Extensions package also opens the door to other reusable Umbraco actions that aren't tied to a specific integration. And that's probably what I find most interesting about Automate: once you start thinking in small triggers, actions and outputs, you quickly start seeing automation opportunities everywhere. Try it The packages are available on NuGet: Umbraco.Community.Automate Umbraco.Community.Automate.Extensions Umbraco.Community.Automate.Cloudflare Source code, examples and issues are available on GitHub: https://github.com/erikjanwestendorp/Umbraco.Community.Automate The project is open source, so feedback, ideas, issues and pull requests are very welcome. If you're experimenting with Umbraco Automate as well, I'd love to hear what kind of integrations or reusable actions you're building.

by Erik-Jan Westendorp

Building my own Umbraco Lamp

I have wanted an UmbLamp ever since Matt Brailsford posted it back in November 2023, but I did not own a 3D printer. That finally changed this July, so I ordered the parts and built one. Here is what I used, what went wrong, and the one mistake worth avoiding before you start.

by Justin Neville

From Examine to Umbraco Search

Demonstrating an approach to migrate a site from Examine to Umbraco Search

by Kenn Jacobsen

Get in contact with us