Awesome-omni-skill umbraco-development
Apply when working with Umbraco CMS, Composers, services, or content APIs
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/frontend/umbraco-development" ~/.claude/skills/diegosouzapw-awesome-omni-skill-umbraco-development && rm -rf "$T"
manifest:
skills/frontend/umbraco-development/SKILL.mdsource content
Bundled Skills
This plugin includes the following additional skills for comprehensive development context:
| Skill | Purpose |
|---|---|
| Razor view syntax, layouts, partials, tag helpers |
| CSS/SASS organization, JavaScript/jQuery patterns (traditional sites) |
| React, Vue, TypeScript patterns (headless/decoupled sites) |
| C#/.NET DI patterns, service architecture, async/await |
| jQuery AJAX integration, form handling (traditional) |
| 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
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
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
// 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
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
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
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)
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
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
[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
[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
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
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
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
{ "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
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
@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
@* 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
@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> }