umbraco-development

Apply when working with Umbraco CMS, Composers, services, or content APIs

16 stars

Best use case

umbraco-development is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Apply when working with Umbraco CMS, Composers, services, or content APIs

Teams using umbraco-development should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/umbraco-development/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/frontend/umbraco-development/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/umbraco-development/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How umbraco-development Compares

Feature / Agentumbraco-developmentStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Apply when working with Umbraco CMS, Composers, services, or content APIs

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

## Bundled Skills

This plugin includes the following additional skills for comprehensive development context:

| Skill | Purpose |
|-------|---------|
| `frontend-razor` | Razor view syntax, layouts, partials, tag helpers |
| `frontend-classic` | CSS/SASS organization, JavaScript/jQuery patterns (traditional sites) |
| `frontend-modern` | React, Vue, TypeScript patterns (headless/decoupled sites) |
| `backend-csharp` | C#/.NET DI patterns, service architecture, async/await |
| `fullstack-classic` | jQuery AJAX integration, form handling (traditional) |
| `fullstack-modern` | REST/GraphQL APIs, Content Delivery API integration (headless) |

These skills are automatically included when you install the Umbraco Analyzer plugin.

# Umbraco Development Patterns

## Project Structure

```
src/
├── Web/                          # Main Umbraco web project
│   ├── App_Plugins/              # Backoffice extensions
│   ├── Composers/                # DI and configuration
│   ├── Controllers/              # Surface and API controllers
│   ├── Views/                    # Razor templates
│   └── Program.cs
├── Core/                         # Business logic (optional)
│   ├── Services/
│   ├── Models/
│   └── Interfaces/
└── Infrastructure/               # Data access (optional)
    ├── Repositories/
    └── ExternalServices/
```

## Composer Pattern

Composers are the entry point for dependency injection and configuration.

### Basic Composer

```csharp
using Umbraco.Cms.Core.Composing;

public class ServicesComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        // Register services
        builder.Services.AddScoped<IProductService, ProductService>();
        builder.Services.AddScoped<ISearchService, SearchService>();

        // Register notification handlers
        builder.AddNotificationHandler<ContentPublishedNotification, ContentPublishedHandler>();
        builder.AddNotificationAsyncHandler<ContentSavingNotification, ContentSavingHandler>();
    }
}
```

### Composer with Dependencies

```csharp
public class ConfiguredServicesComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        // Register with configuration
        builder.Services.Configure<EmailOptions>(
            builder.Config.GetSection("Email"));

        builder.Services.AddScoped<IEmailService, EmailService>();
    }
}
```

### Composer Ordering

```csharp
// Run after another Composer
[ComposeAfter(typeof(ServicesComposer))]
public class DependentComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder) { }
}

// Run before another Composer
[ComposeBefore(typeof(OtherComposer))]
public class EarlyComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder) { }
}
```

## Notification Handlers

### Synchronous Handler

```csharp
public class ContentPublishedHandler : INotificationHandler<ContentPublishedNotification>
{
    private readonly ILogger<ContentPublishedHandler> _logger;

    public ContentPublishedHandler(ILogger<ContentPublishedHandler> logger)
    {
        _logger = logger;
    }

    public void Handle(ContentPublishedNotification notification)
    {
        foreach (var content in notification.PublishedEntities)
        {
            _logger.LogInformation("Content published: {Name}", content.Name);
        }
    }
}
```

### Asynchronous Handler

```csharp
public class ContentSavingHandler : INotificationAsyncHandler<ContentSavingNotification>
{
    private readonly IExternalService _externalService;

    public ContentSavingHandler(IExternalService externalService)
    {
        _externalService = externalService;
    }

    public async Task HandleAsync(ContentSavingNotification notification, CancellationToken ct)
    {
        foreach (var content in notification.SavedEntities)
        {
            await _externalService.SyncContentAsync(content.Key, ct);
        }
    }
}
```

## Content Access

### Using IPublishedContentQuery

```csharp
public class ContentService
{
    private readonly IPublishedContentQuery _contentQuery;

    public ContentService(IPublishedContentQuery contentQuery)
    {
        _contentQuery = contentQuery;
    }

    public IPublishedContent? GetContentByKey(Guid key)
    {
        return _contentQuery.Content(key);
    }

    public IPublishedContent? GetContentByRoute(string route)
    {
        return _contentQuery.ContentSingleAtXPath($"//{route}");
    }

    public IEnumerable<IPublishedContent> GetChildren(IPublishedContent parent)
    {
        return parent.Children.Where(x => x.IsVisible());
    }
}
```

### Using IUmbracoContextFactory (Singleton-Safe)

```csharp
public class SingletonService
{
    private readonly IUmbracoContextFactory _contextFactory;

    public SingletonService(IUmbracoContextFactory contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public IPublishedContent? GetContent(Guid key)
    {
        using var cref = _contextFactory.EnsureUmbracoContext();
        return cref.UmbracoContext?.Content?.GetById(key);
    }
}
```

## Surface Controllers

### Form Handling

```csharp
public class ContactFormController : SurfaceController
{
    private readonly IContactService _contactService;
    private readonly ILogger<ContactFormController> _logger;

    public ContactFormController(
        IUmbracoContextAccessor umbracoContextAccessor,
        IUmbracoDatabaseFactory databaseFactory,
        ServiceContext services,
        AppCaches appCaches,
        IProfilingLogger profilingLogger,
        IPublishedUrlProvider publishedUrlProvider,
        IContactService contactService,
        ILogger<ContactFormController> logger)
        : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
    {
        _contactService = contactService;
        _logger = logger;
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Submit(ContactFormModel model, CancellationToken ct)
    {
        if (!ModelState.IsValid)
        {
            return CurrentUmbracoPage();
        }

        try
        {
            await _contactService.ProcessContactAsync(model, ct);
            TempData["ContactSuccess"] = true;
            return RedirectToCurrentUmbracoPage();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process contact form");
            ModelState.AddModelError("", "An error occurred. Please try again.");
            return CurrentUmbracoPage();
        }
    }
}
```

### AJAX Response

```csharp
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SubmitAjax(ContactFormModel model, CancellationToken ct)
{
    if (!ModelState.IsValid)
    {
        return Json(new
        {
            success = false,
            errors = ModelState.SelectMany(x => x.Value.Errors.Select(e => e.ErrorMessage))
        });
    }

    await _contactService.ProcessContactAsync(model, ct);
    return Json(new { success = true, message = "Thank you for your message!" });
}
```

## API Controllers

### Umbraco API Controller

```csharp
[Route("api/products")]
public class ProductsApiController : UmbracoApiController
{
    private readonly IProductService _productService;

    public ProductsApiController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll(CancellationToken ct)
    {
        var products = await _productService.GetAllAsync(ct);
        return Ok(products);
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var product = await _productService.GetByIdAsync(id, ct);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
}
```

## Examine (Search)

### Basic Search

```csharp
public class SearchService
{
    private readonly IExamineManager _examineManager;

    public SearchService(IExamineManager examineManager)
    {
        _examineManager = examineManager;
    }

    public IEnumerable<ISearchResult> Search(string term)
    {
        if (!_examineManager.TryGetIndex("ExternalIndex", out var index))
        {
            return Enumerable.Empty<ISearchResult>();
        }

        var searcher = index.Searcher;
        var query = searcher.CreateQuery("content")
            .NativeQuery($"+__NodeTypeAlias:blogPost +bodyText:{term}*");

        return query.Execute();
    }
}
```

### Advanced Search with Filters

```csharp
public ISearchResults SearchProducts(string term, string category, int page, int pageSize)
{
    if (!_examineManager.TryGetIndex("ExternalIndex", out var index))
    {
        return EmptySearchResults.Instance;
    }

    var query = index.Searcher.CreateQuery("content")
        .NodeTypeAlias("product");

    if (!string.IsNullOrEmpty(term))
    {
        query = query.And().ManagedQuery(term);
    }

    if (!string.IsNullOrEmpty(category))
    {
        query = query.And().Field("category", category);
    }

    var results = query.Execute(new QueryOptions(
        skip: (page - 1) * pageSize,
        take: pageSize
    ));

    return results;
}
```

## Caching

### Runtime Cache

```csharp
public class CachedContentService
{
    private readonly AppCaches _appCaches;
    private readonly IContentService _contentService;

    public CachedContentService(AppCaches appCaches, IContentService contentService)
    {
        _appCaches = appCaches;
        _contentService = contentService;
    }

    public object GetCachedData(string key)
    {
        return _appCaches.RuntimeCache.GetCacheItem(
            key,
            () => _contentService.GetData(),
            TimeSpan.FromMinutes(5)
        );
    }

    public void ClearCache(string key)
    {
        _appCaches.RuntimeCache.ClearByKey(key);
    }
}
```

## Configuration

### appsettings.json

```json
{
  "Umbraco": {
    "CMS": {
      "Content": {
        "AllowEditInvariantFromNonDefault": true
      },
      "Global": {
        "UseHttps": true
      },
      "ModelsBuilder": {
        "ModelsMode": "SourceCodeAuto"
      }
    }
  },
  "MyApp": {
    "Email": {
      "SmtpHost": "smtp.example.com",
      "SmtpPort": 587,
      "FromAddress": "noreply@example.com"
    }
  }
}
```

### Options Pattern

```csharp
public class EmailOptions
{
    public string SmtpHost { get; set; } = string.Empty;
    public int SmtpPort { get; set; } = 587;
    public string FromAddress { get; set; } = string.Empty;
}

// In Composer
builder.Services.Configure<EmailOptions>(
    builder.Config.GetSection("MyApp:Email"));

// In Service
public class EmailService
{
    private readonly EmailOptions _options;

    public EmailService(IOptions<EmailOptions> options)
    {
        _options = options.Value;
    }
}
```

## Razor Views

### Template with Model

```cshtml
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.BlogPost>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels

@{
    Layout = "Master.cshtml";
}

<article>
    <h1>@Model.Title</h1>
    <p class="meta">
        Published: @Model.PublishDate.ToString("MMMM dd, yyyy")
        by @Model.Author?.Name
    </p>
    <div class="content">
        @Html.Raw(Model.BodyText)
    </div>
</article>
```

### Partial View with Model

```cshtml
@* Views/Partials/_Navigation.cshtml *@
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@{
    var root = Model.Root();
    var items = root.Children.Where(x => x.IsVisible());
}

<nav>
    <ul>
        @foreach (var item in items)
        {
            <li class="@(item.IsAncestorOrSelf(Model) ? "active" : "")">
                <a href="@item.Url()">@item.Name</a>
            </li>
        }
    </ul>
</nav>
```

### Form in View

```cshtml
@using (Html.BeginUmbracoForm<ContactFormController>(nameof(ContactFormController.Submit)))
{
    @Html.AntiForgeryToken()

    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="Message"></label>
        <textarea asp-for="Message" class="form-control"></textarea>
        <span asp-validation-for="Message" class="text-danger"></span>
    </div>

    <button type="submit" class="btn btn-primary">Send</button>
}
```

Related Skills

development

16
from diegosouzapw/awesome-omni-skill

Comprehensive web, mobile, and backend development workflow bundling frontend, backend, full-stack, and mobile development skills for end-to-end application delivery.

vue-3-nuxt-3-development-cursorrules-prompt-file-cursorrules

16
from diegosouzapw/awesome-omni-skill

Apply for vue-3-nuxt-3-development-cursorrules-prompt-file. --- description: Applies to Vue 3 and Nuxt 3 projects, enforcing best practices for frontend development including TypeScript, TailwindCSS, and Composition API. globs: **/*.{vue,ts,js,jsx,tsx}

Frontend Development

16
from diegosouzapw/awesome-omni-skill

พัฒนา Frontend ด้วย Angular, React, Vue, Next.js อย่างมืออาชีพ

App Development

16
from diegosouzapw/awesome-omni-skill

Build features in the AI Coaching Platform Next.js app. Use for creating pages, components, server actions, TanStack tables, and understanding application architecture.

angular-v21-development

16
from diegosouzapw/awesome-omni-skill

Develop Angular v21 components, services, and directives with signals. Use when implementing standalone components, OnPush change detection, inject() function, and input()/output() functions.

analogjs-development

16
from diegosouzapw/awesome-omni-skill

Develop with Analogjs 2.x file-based routing, markdown content management, and SSR/SSG configuration. Use when creating *.page.ts files, contentFilesResource, routeMeta, and prerender settings.

spec_driven_development.constitution

16
from diegosouzapw/awesome-omni-skill

Creates foundational governance principles and development guidelines for the project. Use when starting a new project or establishing standards.

miniprogram-development

16
from diegosouzapw/awesome-omni-skill

WeChat Mini Program development rules. Use this skill when developing WeChat mini programs, integrating CloudBase capabilities, and deploying mini program projects.

Workers Development

16
from diegosouzapw/awesome-omni-skill

This skill should be used when the user asks about "Workers API", "fetch handler", "Workers runtime", "request handling", "response handling", "Workers bindings", "environment variables in Workers", "Workers context", or discusses implementing Workers code, routing patterns, or using Cloudflare bindings like KV, D1, R2, Durable Objects in Workers.

wordpress-woocommerce-development

16
from diegosouzapw/awesome-omni-skill

WooCommerce store development workflow covering store setup, payment integration, shipping configuration, and customization.

wordpress-theme-development

16
from diegosouzapw/awesome-omni-skill

WordPress theme development workflow covering theme architecture, template hierarchy, custom post types, block editor support, and responsive design.

web-development

16
from diegosouzapw/awesome-omni-skill

Web frontend project development rules. Use this skill when developing web frontend pages, deploying static hosting, and integrating CloudBase Web SDK.