orchardcore-admin
Guidance for working with the Orchard Core admin panel, including admin controllers, menu registration, dashboard widgets, admin theme customization, settings pages, and admin-specific shapes and zone
SKILL.mdsrc/CrestApps.AgentSkills/orchardcore/orchardcore-admin/SKILL.md
GitHubOrchard Core Admin Panel
The Orchard Core admin panel is the back-office interface for content management and site configuration. It is rendered by the TheAdmin theme for every request routed through the admin pipeline ([Admin] attribute). Related skills cover the details of specific admin areas:
orchardcore-admin-edit-viewsfor editor views and forms built with theocat-*classes.orchardcore-admin-listsfor admin index (list) pages rendered with theAdminListshape.orchardcore-breadcrumbsfor the<breadcrumb>page title trail.orchardcore-admin-menufor the admin-configurableOrchardCore.AdminMenunode tree.orchardcore-admin-dashboardforDashboardWidgetcontent.
TheAdmin Theme
Layout Zones
| Zone | Purpose |
|---|---|
HeadMeta |
Extra <meta>/<link> tags in <head> |
Title |
Page title; holds the <breadcrumb> tag helper |
Breadcrumbs |
Rendered right above Title; TheAdmin moves visible trails here |
Navigation |
Admin sidebar menu |
Header |
Top of the content area |
Messages |
Notifications and alerts |
Content |
Main body (the view itself) |
Footer |
Bottom of the layout |
<zone Name="Messages">
<div class="alert alert-info">@T["Remember to publish your changes."]</div>
</zone>
Admin Menu Layout (4.0)
- First-level items holding children render as groups (a muted caption above their items). An item that links to a page and has children renders its label as a link plus a separate
button.nav-toggle. - Below the
mdbreakpoint the menu is an off-canvas drawer toggled bybutton.ta-sidebar-togglerand closed bydiv.ta-sidebar-backdrop, <kbd>Esc</kbd>, or opening an item. The compact rail only applies frommdup, where groups open as flyouts. - The section holding the current page always opens. Navigation menu behavior (
AdminSettings.MenuBehavior) decides the rest:Focused(default) opens only the current section and closes siblings;Persistentrestores what each user left open, remembered per user in the browser. - Markup changes for overrides of
Navigation-admin.cshtml,NavigationItem-admin.cshtml, orNavigationItemLink-admin.cshtml: the selectedlialso hascurrent(activemarks it and its ancestors), groupulelements carrynav-group-itemswith anidofm{hash}, first-levellielements carrynav-group, noulrenders withshowand toggles renderaria-expanded="false"(the script applies state),figcaptioncarriesnav-item-row, and the filter input is wrapped indiv.nav-filter. - The admin menu checks each permission/resource once and drops the children of an unauthorized item together with it.
Menu Colors
The menu has its own surface built on the Bootstrap gray scale. Rebrand it by redefining the custom properties in a theme that depends on TheAdmin:
:root {
--oc-admin-accent-rgb: 73, 80, 87;
--oc-admin-accent-strong-color: #343a40;
--oc-admin-menu-bg-color: #f1f3f5;
--oc-admin-menu-border-color: #dee2e6;
}
New properties include --oc-admin-menu-group-font-color, --oc-admin-menu-border-color, --oc-admin-accent-rgb, --oc-admin-accent-color, and --oc-admin-accent-strong-color (darkest gray in light mode, lightest in dark mode). --oc-admin-menu-separator-color, --oc-admin-menu-item-hover-bg-color, and --oc-admin-menu-item-active-bg-color are washes of --oc-admin-accent-rgb, and --oc-selected-indicator-color is now a gray instead of var(--bs-primary). The sidebar uses var(--bs-body-font-family), labels wrap instead of truncating, and animations are disabled under prefers-reduced-motion.
Scripts and Styles
- TheAdmin no longer includes
bootstrap-selectstyles. Pages that usebootstrap-selectmust require them:<style asp-name="bootstrap-select" at="Head"></style>. - Admin scripts no longer depend on jQuery or jQuery UI, and admin Vue apps use Vue 3. Front-end jQuery resources are still registered; require them explicitly (
<script asp-name="jQuery" at="Foot"></script>) if your admin view still needs them. AJAX-injected field and part editors are ES modules.
Admin Editor Forms (ocat-*)
Admin editor views lay out their rows with the static ocat- (Orchard Core Admin Theme) classes (ocat-wrapper, ocat-label, ocat-label-required, ocat-end, ocat-end-offset, ocat-start, ocat-limited-wrapper, ocat-limited), so an admin theme can restyle every form, for example as a horizontal layout, by overriding those classes in CSS. Use them only in admin views, never in front-end views:
<div class="ocat-wrapper" asp-validation-class-for="Title">
<label asp-for="Title" class="ocat-label">@T["Title"]</label>
<div class="ocat-end">
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title"></span>
<span class="hint">@T["Hint text."]</span>
</div>
</div>
Content field hints render through the Hint_Admin shape (TheAdmin's Hint.Admin.cshtml), which a custom admin theme can override. See orchardcore-admin-edit-views for the full class catalog, layout rules, validation, hints, part and field wrapper classes, editor tabs and cards, and complete editor examples, and orchardcore-admin-lists for admin index (list) pages.
Admin Controllers
Apply [Admin] to a controller or action to render it in the admin theme and require admin access. Its optional route template and route name work like [Route], with the admin prefix prepended; the name can contain {area}, {controller}, and {action}. An action-level attribute takes precedence.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using OrchardCore.Admin;
using OrchardCore.DisplayManagement.Notify;
[Admin("Person/{action}/{id?}", "Person{action}")]
public sealed class PersonController : Controller
{
private readonly INotifier _notifier;
private readonly IHtmlLocalizer H;
public PersonController(INotifier notifier, IHtmlLocalizer<PersonController> htmlLocalizer)
{
_notifier = notifier;
H = htmlLocalizer;
}
[Admin("Person", "Person")]
public IActionResult Index() => View();
public IActionResult Edit(string id) => View();
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Update()
{
await _notifier.SuccessAsync(H["Settings updated."]);
return RedirectToAction(nameof(Index));
}
}
With the default prefix, Index is ~/Admin/Person (route Person) and Edit is ~/Admin/Person/{id} (route PersonEdit). Use AdminAttribute.IsApplied(httpContext) to detect admin requests in filters and services.
Admin URL Prefix
{
"OrchardCore": {
"Admin": {
"AdminUrlPrefix": "Backoffice"
}
}
}
Read it through IOptions<AdminOptions> (AdminUrlPrefix). The legacy OrchardCore_Admin section still binds but is deprecated.
Admin Route URL Generation
Prefer tag helpers in views, and LinkGenerator with the current HttpContext in code, so tenant and admin prefixes are preserved:
<a asp-action="Index" asp-controller="Settings" asp-area="MyModule">@T["Open settings"]</a>
using Microsoft.AspNetCore.Routing;
public sealed class AdminUrlService
{
private readonly LinkGenerator _linkGenerator;
public AdminUrlService(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public string GetSettingsUrl(HttpContext httpContext)
=> _linkGenerator.GetPathByAction(httpContext, action: "Index", controller: "Settings", values: new { area = "MyModule" });
}
Do not use @Url.Content("~/...") for application links.
Admin Views
Admin views declare their title with the <breadcrumb> tag helper (add @addTagHelper *, OrchardCore.Navigation.Core to _ViewImports.cshtml). It renders the trail, the <h1>, and the browser title segment, so do not add a separate heading:
<zone Name="Title">
<breadcrumb name="MyModuleSettings" title="@T["My Module Settings"]" />
</zone>
<form asp-action="Update" method="post">
<div class="ocat-wrapper" asp-validation-class-for="DisplayName">
<label asp-for="DisplayName" class="ocat-label">@T["Display Name"]</label>
<div class="ocat-end">
<input asp-for="DisplayName" class="form-control" />
<span asp-validation-for="DisplayName"></span>
</div>
</div>
<div class="ocat-wrapper">
<div class="ocat-end-offset">
<button type="submit" class="btn btn-primary">@T["Save"]</button>
</div>
</div>
</form>
Edit pages and shape-based editors follow orchardcore-admin-edit-views; list pages render their items with <admin-list> (see orchardcore-admin-lists).
Admin Menu Registration
Inherit from AdminNavigationProvider (a NamedNavigationProvider scoped to the admin menu) and register it with AddNavigationProvider<T>():
using Microsoft.Extensions.Localization;
using OrchardCore.Navigation;
internal sealed class AdminMenu : AdminNavigationProvider
{
private readonly IStringLocalizer S;
public AdminMenu(IStringLocalizer<AdminMenu> localizer)
{
S = localizer;
}
protected override ValueTask BuildAsync(NavigationBuilder builder)
{
builder
.Add(S["Content"], NavigationConstants.AdminMenuContentPosition, content => content
.Add(S["Taxonomies"], S["Taxonomies"].PrefixPosition(), taxonomies => taxonomies
.Permission(MyModulePermissions.ManageTaxonomies)
.Action("Index", "Admin", "MyModule")
.LocalNav()
)
)
.Add(S["Settings"], settings => settings
.Add(S["My Module"], S["My Module"].PrefixPosition(), entry => entry
.Id("mymodule")
.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = "mymodule" })
.Permission(MyModulePermissions.ManageMyModuleSettings)
.LocalNav()
)
);
return ValueTask.CompletedTask;
}
}
services.AddNavigationProvider<AdminMenu>();
- Standard top-level groups are Content, Design, Search, Settings, Tools, and Access Control. Positions that remain in
NavigationConstantsareAdminMenuContentPosition,AdminMenuDesignPosition,AdminMenuSearchPosition, andAdminMenuAccessControlPosition. Settings pages go under Settings (often in a sub-group such as Communication), utilities under Tools. PrefixPosition()sorts items alphabetically by their localized text. NestAddcalls for sub-groups.Permission(...)hides an item from users who lack it (chain several for "any of"); hiding a parent also hides its children. Use.Resource(...)for resource-based checks.- Use
INavigationProviderdirectly only for providers that contribute to several named menus.
Adding an Icon to an Admin Menu Item
Do not put Font Awesome classes on items with AddClass(...). Give the item an id and add Views/NavigationItemText-{id}.Id.cshtml:
builder.Add(S["Sports"], sports => sports
.Id("sports")
.Add(S["Calendar"], calendar => calendar
.Action("Index", "Admin", "MyModule")
.LocalNav()
)
);
<span class="icon">
<i class="fa-regular fa-futbol" aria-hidden="true"></i>
</span>
<span class="title">@Model.Text</span>
Quick Navigation (Ctrl+K)
OrchardCore.Admin provides a quick navigation palette opened with <kbd>Ctrl</kbd>+<kbd>K</kbd> (<kbd>Cmd</kbd>+<kbd>K</kbd> on macOS) or the navbar magnifier. It filters the admin menu items the user can access (case- and accent-insensitive, including parent names shown as a breadcrumb). It is enabled by default; turn it off with Enable quick navigation in Configuration → Settings → Admin or AdminSettings.DisplayQuickNavigation.
- The palette loads a fresh, localized, permission-filtered JSON index from the
AdminQuickNavigationIndexroute (~/Admin/QuickNavigation/Index, requiresAccessAdminPanel,404when disabled) on every open, withCache-Control: no-store. Entries havesource,id,title,path,href, andtarget. - Admin themes that render the
Navbarshape with theDetailAdmindisplay type inherit the palette (QuickNavigationNavbarItemshape plusadmin-quick-navigationscript and style). Restyle it after that stylesheet:<style asp-name="my-admin-theme" asp-src="~/MyAdminTheme/Styles/theme.css" depends-on="admin-quick-navigation" at="Head"></style>. - Enable Content Quick Navigation (
OrchardCore.Contents.QuickNavigation) to add edit links for the most recently updated content items the user can edit;ContentQuickNavigationSettings.MaxItems(default50) is configurable in the Admin settings.
Adding a Source
Derive from QuickNavigationSource (OrchardCore.Admin.QuickNavigation in OrchardCore.Admin.Abstractions) in an opt-in feature that depends on OrchardCore.Admin:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using OrchardCore.Admin.QuickNavigation;
public sealed class ReportNavigationSource : QuickNavigationSource
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuthorizationService _authorizationService;
private readonly LinkGenerator _linkGenerator;
private readonly IReportStore _reportStore;
private readonly IStringLocalizer S;
public ReportNavigationSource(
IHttpContextAccessor httpContextAccessor,
IAuthorizationService authorizationService,
LinkGenerator linkGenerator,
IReportStore reportStore,
IStringLocalizer<ReportNavigationSource> localizer)
{
_httpContextAccessor = httpContextAccessor;
_authorizationService = authorizationService;
_linkGenerator = linkGenerator;
_reportStore = reportStore;
S = localizer;
}
public override string Name => "Reports";
public override async ValueTask<IEnumerable<string>> GetEntryIdsAsync()
=> await _reportStore.GetRecentIdsAsync(20);
public override async ValueTask<QuickNavigationResult> DisplayAsync(string entryId)
{
var httpContext = _httpContextAccessor.HttpContext;
var report = await _reportStore.GetAsync(entryId);
if (report is null || !await _authorizationService.AuthorizeAsync(httpContext.User, ReportPermissions.ViewReports, report))
{
return null;
}
return new QuickNavigationResult
{
Title = report.Name,
Path = [S["Reports"].Value],
Href = _linkGenerator.GetPathByAction(httpContext, "View", "Report", new { area = "My.Reports", id = report.Id }),
};
}
}
[Feature("My.Reports.QuickNavigation")]
public sealed class QuickNavigationStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<QuickNavigationSource, ReportNavigationSource>();
}
}
Namemust be unique, stable, and culture-independent.SourceandIdof the result are assigned by the endpoint; returnnullfor missing or unauthorized entries.- For expensive sources, keep identifiers (never localized labels or pre-authorized results) in the tenant-singleton, in-memory
IQuickNavigationIndexfrom a background task or event handler:index.Replace("Reports", ids)atomically replaces that source's partition (an empty collection clears it). It is not persisted or distributed, so repopulate it after restarts and on every server. - Always authorize again and generate links from the current request in
DisplayAsync.
Navbar Shape
The Navbar shape is rendered with DetailAdmin on the admin and Detail on the front end. Add items with a display driver:
using OrchardCore.Admin.Models;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
public sealed class HelpNavbarDisplayDriver : DisplayDriver<Navbar>
{
public override IDisplayResult Display(Navbar model, BuildDisplayContext context)
{
return View("HelpNavbarItem", model)
.Location(OrchardCoreConstants.DisplayType.DetailAdmin, "Content:25");
}
}
Register it with services.AddDisplayDriver<Navbar, HelpNavbarDisplayDriver>().
Admin Branding
Override the AdminBranding shape (from a custom admin theme or with Admin Templates) to change the logo, site name, and favicon:
<zone Name="HeadMeta">
<link asp-src="~/media/favicon.ico" type="image/x-icon" rel="shortcut icon" />
</zone>
<a class="ta-navbar-brand" asp-route-area="OrchardCore.Admin" asp-controller="Admin" asp-action="Index">
<div class="d-flex align-items-center">
<img src="~/media/logo.png" alt="@Site.SiteName" class="pe-2" />
<span>@Site.SiteName</span>
</div>
</a>
Admin Home Page Content
Without OrchardCore.AdminDashboard, /admin renders the AdminDashboard shape, which displays an AdminDashboardContent child shape. Features add positioned content with a shape table provider:
using OrchardCore.DisplayManagement.Descriptors;
using OrchardCore.DisplayManagement.Views;
public sealed class AdminDashboardShapeTableProvider : ShapeTableProvider
{
public override ValueTask DiscoverAsync(ShapeTableBuilder builder)
{
builder.Describe("AdminDashboardContent")
.OnDisplaying(async displaying =>
{
await displaying.Shape.AddAsync(new ShapeViewModel("MyAdminDashboardContent"), "10");
});
return ValueTask.CompletedTask;
}
}
Register it with services.AddShapeTableProvider<AdminDashboardShapeTableProvider>() and add a MyAdminDashboardContent.cshtml template.
Admin Settings Pages
Store settings on the site and edit them with a SiteDisplayDriver<T>:
public sealed class NotificationSettings
{
public bool EnableEmailNotifications { get; set; }
public string DefaultRecipient { get; set; }
}
using OrchardCore.DisplayManagement.Entities;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Settings;
public sealed class NotificationSettingsDisplayDriver : SiteDisplayDriver<NotificationSettings>
{
public const string GroupId = "notifications";
protected override string SettingsGroupId => GroupId;
public override IDisplayResult Edit(ISite site, NotificationSettings settings, BuildEditorContext context)
{
return Initialize<NotificationSettingsViewModel>("NotificationSettings_Edit", model =>
{
model.EnableEmailNotifications = settings.EnableEmailNotifications;
model.DefaultRecipient = settings.DefaultRecipient;
}).Location("Content:5")
.OnGroup(SettingsGroupId);
}
public override async Task<IDisplayResult> UpdateAsync(ISite site, NotificationSettings settings, UpdateEditorContext context)
{
var model = new NotificationSettingsViewModel();
await context.Updater.TryUpdateModelAsync(model, Prefix);
settings.EnableEmailNotifications = model.EnableEmailNotifications;
settings.DefaultRecipient = model.DefaultRecipient;
return Edit(site, settings, context);
}
}
public class NotificationSettingsViewModel
{
public bool EnableEmailNotifications { get; set; }
public string DefaultRecipient { get; set; }
}
services.AddSiteDisplayDriver<NotificationSettingsDisplayDriver>();
Link it from the Settings group with .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = NotificationSettingsDisplayDriver.GroupId }). When options are populated from these settings, consume them through IOptionsMonitor<T>.CurrentValue and refresh them with IOptionsChangeTokenSource<T> plus IOptionsUpdateNotifier (see orchardcore-site-settings).
Admin Settings Reference
Configuration → Settings → Admin requires AdminPermissions.ManageAdminSettings; the admin itself requires AdminPermissions.AccessAdminPanel.
{
"steps": [
{
"name": "settings",
"AdminSettings": {
"DisplayThemeToggler": true,
"DisplayMenuFilter": true,
"DisplayQuickNavigation": true,
"DisplayNewMenu": true,
"DisplayTitlesInTopbar": false,
"ShowBreadcrumb": true,
"MenuBehavior": "Persistent",
"ListLayout": "Grid",
"ListActionsLayout": "Menu",
"AllowUserListLayoutSelection": true
},
"ContentQuickNavigationSettings": {
"MaxItems": 25
}
}
]
}
| Property | Default | Description |
|---|---|---|
DisplayThemeToggler |
true |
Light/dark theme toggler. |
DisplayMenuFilter |
false |
Filter input above the admin menu. |
DisplayQuickNavigation |
true |
Ctrl+K quick navigation palette. |
DisplayNewMenu |
false |
"New" content menu in the admin navigation. |
DisplayTitlesInTopbar |
false |
Page titles in the top bar. |
ShowBreadcrumb |
true |
Admin breadcrumb trails (see orchardcore-breadcrumbs). |
MenuBehavior |
Focused |
Focused or Persistent menu sections. |
ListLayout |
null |
List or Grid; null uses AdminListOptions.DefaultLayout (see orchardcore-admin-lists). |
ListActionsLayout |
null |
Buttons or Menu; null uses AdminListOptions.DefaultActionsLayout. |
AllowUserListLayoutSelection |
false |
Per-user List/Grid selector on every list. |
Admin Filters
Run logic for every admin page with an MVC result filter guarded by AdminAttribute.IsApplied:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using OrchardCore.Admin;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Layout;
public sealed class AdminBannerFilter : IAsyncResultFilter
{
private readonly ILayoutAccessor _layoutAccessor;
private readonly IShapeFactory _shapeFactory;
public AdminBannerFilter(ILayoutAccessor layoutAccessor, IShapeFactory shapeFactory)
{
_layoutAccessor = layoutAccessor;
_shapeFactory = shapeFactory;
}
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is ViewResult && AdminAttribute.IsApplied(context.HttpContext))
{
var layout = await _layoutAccessor.GetLayoutAsync();
await layout.Zones["Messages"].AddAsync(await _shapeFactory.CreateAsync("AdminBanner"), "0");
}
await next();
}
}
services.Configure<MvcOptions>(options => options.Filters.Add<AdminBannerFilter>());
The same pattern can require an admin stylesheet defined in a resource manifest (_resourceManager.RequireStyle("MyModule.AdminStyles")).
Upgrading from 3.x
- Remove
AppContext.SetSwitch("LegacyAdminMenuNavigation", true); the legacy admin menu layout is gone.NavigationHelper.UseLegacyFormat(),NavigationConstants.AdminMenuSecurityPosition,AdminMenuAuditTrailPosition,AdminMenuWorkflowsPosition, andAdminMenuConfigurationPositionwere removed; use the Settings, Tools, and Access Control groups. OrchardCore.Admin.PermissionsAdminSettings.ManageAdminSettingswas removed; useOrchardCore.Admin.AdminPermissions.ManageAdminSettings.- Replace
<zone Name="Title"><h1>@RenderTitleSegments(...)</h1></zone>with the<breadcrumb>tag helper. - Re-check overrides of the admin menu templates and custom menu CSS against the new markup and color variables; require
bootstrap-selectstyles explicitly. - Overridden list views must render
<admin-list>(seeorchardcore-admin-lists). - Use
OrchardCore:Admininstead ofOrchardCore_Adminin configuration.
For practical end-to-end examples, see references/admin-examples.md.