Creating a theme
A GrandNode storefront theme is a plugin in the Themes group. It does not fork the storefront: it overrides only the Razor views it ships and every other view falls through to the defaults in Grand.Web/Views. The shipped Theme.Modern and Theme.Nordic are complete references; this article explains the mechanism and the files you need.
The files
src/Plugins/Theme.Example/
Theme.Example.csproj
Manifest.cs Group = "Themes", SystemName = "Theme.Example"
ExampleThemePlugin.cs BasePlugin
ExampleThemeView.cs IThemeView - the whole mechanism
StartupApplication.cs registers the IThemeView
logo.jpg plugin list logo
Content/ CSS, JS, fonts, images, theme.jpg
Views/Example/ the overridden views
_ViewImports.cshtml
_ViewStart.cshtml
Shared/...
Product/...
A theme without settings needs no install logic:
public class ExampleThemePlugin : BasePlugin, IPlugin;
And one registration:
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IThemeView, ExampleThemeView>();
}
IThemeView and the fallback
public class ExampleThemeView : IThemeView
{
public string AreaName => "";
public string ThemeName => "Example";
public ThemeInfo ThemeInfo => new("Example theme",
"~/Plugins/Theme.Example/Content/theme.jpg", "A short description", false);
public IEnumerable<string> GetViewLocations()
{
return new List<string> {
"/Views/Example/{1}/{0}.cshtml",
"/Views/Example/Shared/{0}.cshtml",
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml"
};
}
}
| Member | What it does |
|---|---|
AreaName | Empty for storefront themes. |
ThemeName | The key stored in settings. It must equal the folder name under Views/, because the location strings contain it. |
ThemeInfo | Title, preview image, preview text and whether the theme supports right-to-left languages. The admin offers only RTL-capable themes for RTL stores. |
GetViewLocations() | Ordered view locations; {0} is the view name, {1} the controller name. The first match wins. |
The last two locations are the fallback. With them, a request for Product/ProductTemplate.Simple uses /Views/Example/Product/ProductTemplate.Simple.cshtml if the theme has it and the default view otherwise. Remove them and every page the theme has not copied fails with "view not found".
Overriding views
- Copy
_ViewImports.cshtmlfrom an existing theme intoViews/Example/. A theme folder does not inheritGrand.Web's imports. Keep the line that removes the default input tag helper, otherwise every checkbox renders twice:@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.InputTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers - Add
_ViewStart.cshtml. - Copy the views you want to change from
src/Web/Grand.Web/Viewsinto the same relative path underViews/Example/. Start with layouts and shared partials — that is where the look of a theme lives.Theme.Modernoverrides about 110 of the 235 default views, most of them inShared/. - Keep the
@model, route names and storefront data attributes (add to cart, wishlist, compare, quick view) of every view you copy, and keep its widget zones:
Dropping a zone silently disables every widget plugin on that page.@await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_top" })
A theme never changes view models, controllers or routes. If a theme needs data the storefront does not provide, the change belongs in Grand.Web.
Content assets
CSS, JavaScript, fonts and images go under Content/ and are referenced as ~/Plugins/Theme.Example/Content/.... Mark them to be copied in the project file, and do not load them from external CDNs:
<ItemGroup>
<None Update="Content\**\*.*"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></None>
<None Update="logo.jpg"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></None>
</ItemGroup>
A theme also references Grand.Web to compile against the storefront view models, with <Private>false</Private> and <ExcludeAssets>all</ExcludeAssets> so the host assembly is not copied. The rest of the project file follows Writing a plugin: Razor SDK, AddRazorSupportForMvc, StaticWebAssetsEnabled=false and an output path to Grand.Web/Plugins/Theme.Example for both Debug and Release.
Prefer CSS over copying a view. Every copied view has to be compared with its upstream version on each GrandNode upgrade; a stylesheet rule does not.
Activating the theme
- Build the theme and install it under Plugins → Local plugins (group Themes).
- Open Settings → General and choose the theme in Default store theme. The setting is store-scoped, so each store can use a different theme.
- Optionally enable Allow customers to select a theme. The customer's choice is kept in a cookie and wins over the default.
Razor views of a theme are compiled into its DLL: after editing a view, rebuild the theme and restart the site.
Tips and common mistakes
| Symptom | Cause |
|---|---|
| Theme selected but pages show the default markup | ThemeName does not match the Views/ folder, or the IThemeView is not registered. |
| Some pages fail with "view not found" | The fallback locations are missing from GetViewLocations(). |
| Tag helpers are not resolved in theme views | No _ViewImports.cshtml in the theme folder. |
| Widgets disappear on one page | A widget zone was dropped from a copied view. |
| Theme CSS returns 404 | Content/** is not copied to the output. |
Related
- Writing a plugin
- Plugin provider types — widgets add markup to zones without a theme.
Question not answered here? Ask the community on GitHub Discussions ↗