Themes - Code Structure of a Theme
Internally a theme only needs the following 4 things:
- An own namespace such as
YourCompany.Themes.YourTheme. - A
ThemeInfo.csfile in that namespace which implementsOqtane.Themes.ITheme.
This file contains information about the theme like name, author, version, etc. - A theme file like
MyTheme.razorwhich is the main thing shown to the user. - A container file like
Container.razorwhich is the main wrapper around a module.
Warning
It is crucial that the namespace of the theme is unique, and that all these core elements (ThemeInfo, Theme, Container) are in exactly this namespace.
Example from the Oqtane Arsha
Arsha is a sample theme that comes with Oqtane. You can find the git repo here.
ThemeInfo.cs
The ThemeInfo.cs file is necessary to provide information about the theme. In the code you can see it contains
- The
namespaceof the theme - must be unique, and must match the theme/container namespace - the
Nameof the theme - displayed to the user - the
Versionof the theme - the
PackageName- used for installation / differentiation - the optional
ThemeSettingsType(class name) of the theme settings - the optional
ContainerSettingsType(class name) of the container settings - a list of
Resources- CSS and JS files that are loaded when the theme is used
note: theResourcescan also be specified in eachTheme.razorfile
using System.Collections.Generic;
using Oqtane.Models;
using Oqtane.Themes;
using Oqtane.Shared;
namespace Oqtane.Theme.Arsha
{
public class ThemeInfo : ITheme
{
public Models.Theme Theme => new Models.Theme
{
Name = "Arsha",
Version = "1.0.0",
PackageName = "Oqtane.Theme.Arsha",
ThemeSettingsType = "Oqtane.Theme.Arsha.ThemeSettings, Oqtane.Theme.Arsha.Client.Oqtane",
ContainerSettingsType = "Oqtane.Theme.Arsha.ContainerSettings, Oqtane.Theme.Arsha.Client.Oqtane",
Resources = new List<Resource>()
{
// Google Fonts
new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i%7CRaleway:300,300i,400,400i,500,500i,600,600i,700,700i%7CPoppins:300,300i,400,400i,500,500i,600,600i,700,700i" },
// Vendor CSS Files
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/assets/vendor/aos/aos.css" },
// ...rest of files omitted for brevity
// Template Main CSS File
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/assets/css/style.css" },
// Vendor JS Files
new Resource { ResourceType = ResourceType.Script, Url = "~/assets/vendor/aos/aos.js", Location = ResourceLocation.Body },
// ...rest of files omitted for brevity
// Template Main JS File
new Resource { ResourceType = ResourceType.Script, Url = "~/assets/js/main.js", Location = ResourceLocation.Body, Reload = true, RenderMode = RenderModes.Static },
new Resource { ResourceType = ResourceType.Script, Url = "~/assets/js/interop.js", Location = ResourceLocation.Body, RenderMode = RenderModes.Interactive }
}
};
}
}
The Theme File
The Theme.razor file is the main file that is shown to the user.
It contains the HTML structure of the theme and can include other Blazor components. Important things to know:
- The
@namespacedirective must match the namespace of the theme - The
@inheritsdirective must inherit fromThemeBaseor a class that inherits fromThemeBase - The
@injectdirective can be used to inject services likeNavigationManagerorISettingService - The
@codeblock can contain C# code that is executed when the component is rendered - The HTML part - which is the HTML inside the
<body>tag (without<head>etc.). This "html" can be- normal HTML such as
div,section,h1,p,imgetc. - Razor variables such as
@PageState.Page.Name - Razor data from functions such as
@NavigateUrl() - Razor statements such as
@ifor@foreach - Blazor components such as
<NavMenu />or<Login />
- normal HTML such as
@namespace Oqtane.Theme.Arsha
@inherits ThemeBase
@inject NavigationManager NavigationManager
@inject ISettingService SettingService
<!-- ======= Header ======= -->
<header id="header" class="@_headerclass">
<div class="container d-flex align-items-center">
<!-- omited for brevity -->
<nav id="navbar" class="navbar">
<NavMenu />
</nav>
<div class="mx-3 controls-group"><UserProfile /> <Login /> <ControlPanel /></div>
</div>
</header>
<!-- End Header -->
@if (!string.IsNullOrEmpty(_hero))
{
<!-- ======= Hero Section ======= -->
@((MarkupString)_hero)
<!-- End Hero -->
<!-- ======= Main ======= -->
<main id="main">
<Pane Name="@PaneNames.Default" />
</main>
<!-- End #main -->
}
else
{
<!-- ======= Main ======= -->
<main id="main">
<!-- ======= Breadcrumbs ======= -->
<section class="breadcrumbs">
<div class="container">
<h2 class="mt-2">@PageState.Page.Name</h2>
</div>
</section><!-- End Breadcrumbs -->
<section class="inner-page">
<div class="container">
<Pane Name="@PaneNames.Default" />
</div>
</section>
</main>
<!-- End Main -->
}
<!-- ======= Footer ======= -->
@((MarkupString)_footer)
<!-- End Footer -->
<a href="@NavigateUrl()" class="back-to-top d-flex align-items-center justify-content-center" data-enhance-nav="false"><i class="bi bi-arrow-up-short"></i></a>
@code {
public override string Name => "Default";
public override string Panes => PaneNames.Default;
private string _hero = "";
private string _headerclass = "";
private string _footer = "";
protected override async Task OnParametersSetAsync()
{
// omitted for brevity
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// omitted for brevity
}
private string DefaultFooter
{
get
{
return
"<footer id=\"footer\">\n" +
" <div class=\"footer-newsletter\">\n" +
<!-- omitted for brevity -->
" </div>\n" +
"</footer>\n";
}
}
}
The Container File
The Container.razor file is the main wrapper around a module. Here's what you should know:
- The
@namespacedirective must match the namespace of the theme - The
@inheritsdirective must inherit fromContainerBaseor a class that inherits fromContainerBase - The
@injectdirective can be used to inject services likeISettingService - The
@codeblock can contain C# code that is executed when the component is rendered - The HTML parts with the same possibilities as the theme above.
@namespace Oqtane.Theme.Arsha
@inherits ContainerBase
@inject ISettingService SettingService
@if (_title)
{
@if (_animate)
{
<section>
<div class="container" data-aos="fade-up">
<ModuleActions />
@if (ModuleState.Title != "-")
{
<div class="section-title">
<h2>@ModuleState.Title</h2>
</div>
}
<div class="row">
<ModuleInstance />
</div>
</div>
</section>
}
else
{
<!-- omitted for brevity -->
}
}
else
{
<!-- omitted for brevity -->
}
@code {
private bool _title = true;
private bool _animate = false;
protected override void OnParametersSet()
{
// omitted for brevity
}
}
Blazor Components
In addition to the required minimum files, you can also include other Blazor components. Here's an example of the NavMenu.razor.
It doesn't do much, but check if the menu should show, and then calls the NavMenuItems component.
@namespace Oqtane.Theme.Arsha
@inherits MenuBase
@if (MenuPages.Any())
{
<NavMenuItems ParentPage="null" Pages="MenuPages" />
<i class="bi bi-list mobile-nav-toggle"></i>
}
The NavMenuItems.razor is way more complex, as it recursively calls itself to render the menu items.
@namespace Oqtane.Theme.Arsha
@inherits MenuItemsBase
<ul>
@foreach (var childPage in GetChildPages())
{
var _attributes = new Dictionary<string, object>();
_attributes.Add("href", GetUrl(childPage));
var _target = GetTarget(childPage);
if (!string.IsNullOrEmpty(_target))
_attributes.Add("target", _target);
if (!childPage.HasChildren)
{
<!-- omitted for brevity -->
}
else
{
<li class="dropdown">
<a @attributes="_attributes"><span>@childPage.Name</span>
@if (childPage.Level == 0)
<i class="bi bi-chevron-down"></i>
else
<i class="bi bi-chevron-right"></i>
</a>
<NavMenuItems ParentPage="childPage" Pages="Pages" />
</li>
}
}
</ul>
Daniel Mettler, @iJungleboy [MS MVP, Oqtane Core Team]
Content Management Expert, Chief Architect of 2sxc and cre8magic.
Forged in the jungles of Indonesia, lives in Switzerland , loves Oqtane 🩸 & 2sxc 💜.
LinkedIn | Discord: @iJungleboy | Twitter: @iJungleboy | Github: @iJungleboy