{"ArticleId":null,"Name":"Creating a theme","Content":"\u003Cp\u003EA GrandNode storefront theme is a plugin in the \u003Ccode\u003EThemes\u003C/code\u003E 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 \u003Ccode\u003EGrand.Web/Views\u003C/code\u003E. The shipped \u003Ccode\u003ETheme.Modern\u003C/code\u003E and \u003Ccode\u003ETheme.Nordic\u003C/code\u003E are complete references; this article explains the mechanism and the files you need.\u003C/p\u003E\n\n\u003Ch2 id=\u0022files\u0022\u003EThe files\u003C/h2\u003E\n\u003Cpre\u003E\u003Ccode\u003Esrc/Plugins/Theme.Example/\n  Theme.Example.csproj\n  Manifest.cs               Group = \u0022Themes\u0022, SystemName = \u0022Theme.Example\u0022\n  ExampleThemePlugin.cs     BasePlugin\n  ExampleThemeView.cs       IThemeView - the whole mechanism\n  StartupApplication.cs     registers the IThemeView\n  logo.jpg                  plugin list logo\n  Content/                  CSS, JS, fonts, images, theme.jpg\n  Views/Example/            the overridden views\n    _ViewImports.cshtml\n    _ViewStart.cshtml\n    Shared/...\n    Product/...\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EA theme without settings needs no install logic:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic class ExampleThemePlugin : BasePlugin, IPlugin;\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EAnd one registration:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic void ConfigureServices(IServiceCollection services, IConfiguration configuration)\n{\n    services.AddScoped\u0026lt;IThemeView, ExampleThemeView\u0026gt;();\n}\u003C/code\u003E\u003C/pre\u003E\n\n\u003Ch2 id=\u0022ithemeview\u0022\u003EIThemeView and the fallback\u003C/h2\u003E\n\u003Cpre\u003E\u003Ccode\u003Epublic class ExampleThemeView : IThemeView\n{\n    public string AreaName =\u0026gt; \u0022\u0022;\n    public string ThemeName =\u0026gt; \u0022Example\u0022;\n\n    public ThemeInfo ThemeInfo =\u0026gt; new(\u0022Example theme\u0022,\n        \u0022~/Plugins/Theme.Example/Content/theme.jpg\u0022, \u0022A short description\u0022, false);\n\n    public IEnumerable\u0026lt;string\u0026gt; GetViewLocations()\n    {\n        return new List\u0026lt;string\u0026gt; {\n            \u0022/Views/Example/{1}/{0}.cshtml\u0022,\n            \u0022/Views/Example/Shared/{0}.cshtml\u0022,\n            \u0022/Views/{1}/{0}.cshtml\u0022,\n            \u0022/Views/Shared/{0}.cshtml\u0022\n        };\n    }\n}\u003C/code\u003E\u003C/pre\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003EMember\u003C/th\u003E\u003Cth\u003EWhat it does\u003C/th\u003E\u003C/tr\u003E\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EAreaName\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EEmpty for storefront themes.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EThemeName\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EThe key stored in settings. It must equal the folder name under \u003Ccode\u003EViews/\u003C/code\u003E, because the location strings contain it.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EThemeInfo\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003ETitle, preview image, preview text and whether the theme supports right-to-left languages. The admin offers only RTL-capable themes for RTL stores.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003E\u003Ccode\u003EGetViewLocations()\u003C/code\u003E\u003C/td\u003E\u003Ctd\u003EOrdered view locations; \u003Ccode\u003E{0}\u003C/code\u003E is the view name, \u003Ccode\u003E{1}\u003C/code\u003E the controller name. The first match wins.\u003C/td\u003E\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\n\u003Cp\u003EThe last two locations are the fallback. With them, a request for \u003Ccode\u003EProduct/ProductTemplate.Simple\u003C/code\u003E uses \u003Ccode\u003E/Views/Example/Product/ProductTemplate.Simple.cshtml\u003C/code\u003E if the theme has it and the default view otherwise. Remove them and every page the theme has not copied fails with \u0022view not found\u0022.\u003C/p\u003E\n\n\u003Ch2 id=\u0022views\u0022\u003EOverriding views\u003C/h2\u003E\n\u003Col\u003E\n\u003Cli\u003ECopy \u003Ccode\u003E_ViewImports.cshtml\u003C/code\u003E from an existing theme into \u003Ccode\u003EViews/Example/\u003C/code\u003E. A theme folder does not inherit \u003Ccode\u003EGrand.Web\u003C/code\u003E\u0027s imports. Keep the line that removes the default input tag helper, otherwise every checkbox renders twice:\n\u003Cpre\u003E\u003Ccode\u003E@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.InputTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers\u003C/code\u003E\u003C/pre\u003E\u003C/li\u003E\n\u003Cli\u003EAdd \u003Ccode\u003E_ViewStart.cshtml\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003ECopy the views you want to change from \u003Ccode\u003Esrc/Web/Grand.Web/Views\u003C/code\u003E into the same relative path under \u003Ccode\u003EViews/Example/\u003C/code\u003E. Start with layouts and shared partials \u2014 that is where the look of a theme lives. \u003Ccode\u003ETheme.Modern\u003C/code\u003E overrides about 110 of the 235 default views, most of them in \u003Ccode\u003EShared/\u003C/code\u003E.\u003C/li\u003E\n\u003Cli\u003EKeep the \u003Ccode\u003E@model\u003C/code\u003E, route names and storefront data attributes (add to cart, wishlist, compare, quick view) of every view you copy, and keep its widget zones:\n\u003Cpre\u003E\u003Ccode\u003E@await Component.InvokeAsync(\u0022Widget\u0022, new { widgetZone = \u0022productdetails_top\u0022 })\u003C/code\u003E\u003C/pre\u003E\nDropping a zone silently disables every widget plugin on that page.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003EA theme never changes view models, controllers or routes. If a theme needs data the storefront does not provide, the change belongs in \u003Ccode\u003EGrand.Web\u003C/code\u003E.\u003C/p\u003E\n\n\u003Ch2 id=\u0022assets\u0022\u003EContent assets\u003C/h2\u003E\n\u003Cp\u003ECSS, JavaScript, fonts and images go under \u003Ccode\u003EContent/\u003C/code\u003E and are referenced as \u003Ccode\u003E~/Plugins/Theme.Example/Content/...\u003C/code\u003E. Mark them to be copied in the project file, and do not load them from external CDNs:\u003C/p\u003E\n\u003Cpre\u003E\u003Ccode\u003E\u0026lt;ItemGroup\u0026gt;\n  \u0026lt;None Update=\u0022Content\\**\\*.*\u0022\u0026gt;\u0026lt;CopyToOutputDirectory\u0026gt;PreserveNewest\u0026lt;/CopyToOutputDirectory\u0026gt;\u0026lt;/None\u0026gt;\n  \u0026lt;None Update=\u0022logo.jpg\u0022\u0026gt;\u0026lt;CopyToOutputDirectory\u0026gt;PreserveNewest\u0026lt;/CopyToOutputDirectory\u0026gt;\u0026lt;/None\u0026gt;\n\u0026lt;/ItemGroup\u0026gt;\u003C/code\u003E\u003C/pre\u003E\n\u003Cp\u003EA theme also references \u003Ccode\u003EGrand.Web\u003C/code\u003E to compile against the storefront view models, with \u003Ccode\u003E\u0026lt;Private\u0026gt;false\u0026lt;/Private\u0026gt;\u003C/code\u003E and \u003Ccode\u003E\u0026lt;ExcludeAssets\u0026gt;all\u0026lt;/ExcludeAssets\u0026gt;\u003C/code\u003E so the host assembly is not copied. The rest of the project file follows \u003Ca href=\u0022/developers-writing-a-plugin\u0022\u003EWriting a plugin\u003C/a\u003E: Razor SDK, \u003Ccode\u003EAddRazorSupportForMvc\u003C/code\u003E, \u003Ccode\u003EStaticWebAssetsEnabled=false\u003C/code\u003E and an output path to \u003Ccode\u003EGrand.Web/Plugins/Theme.Example\u003C/code\u003E for both Debug and Release.\u003C/p\u003E\n\u003Cp\u003EPrefer 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.\u003C/p\u003E\n\n\u003Ch2 id=\u0022activate\u0022\u003EActivating the theme\u003C/h2\u003E\n\u003Col\u003E\n\u003Cli\u003EBuild the theme and install it under \u003Cstrong\u003EPlugins \u2192 Local plugins\u003C/strong\u003E (group \u003Cem\u003EThemes\u003C/em\u003E).\u003C/li\u003E\n\u003Cli\u003EOpen \u003Cstrong\u003ESettings \u2192 General\u003C/strong\u003E and choose the theme in \u003Cstrong\u003EDefault store theme\u003C/strong\u003E. The setting is store-scoped, so each store can use a different theme.\u003C/li\u003E\n\u003Cli\u003EOptionally enable \u003Cstrong\u003EAllow customers to select a theme\u003C/strong\u003E. The customer\u0027s choice is kept in a cookie and wins over the default.\u003C/li\u003E\n\u003C/ol\u003E\n\u003Cp\u003ERazor views of a theme are compiled into its DLL: after editing a view, rebuild the theme and restart the site.\u003C/p\u003E\n\n\u003Ch2 id=\u0022tips\u0022\u003ETips and common mistakes\u003C/h2\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\u003Ctr\u003E\u003Cth\u003ESymptom\u003C/th\u003E\u003Cth\u003ECause\u003C/th\u003E\u003C/tr\u003E\u003C/thead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\u003Ctd\u003ETheme selected but pages show the default markup\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EThemeName\u003C/code\u003E does not match the \u003Ccode\u003EViews/\u003C/code\u003E folder, or the \u003Ccode\u003EIThemeView\u003C/code\u003E is not registered.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003ESome pages fail with \u0022view not found\u0022\u003C/td\u003E\u003Ctd\u003EThe fallback locations are missing from \u003Ccode\u003EGetViewLocations()\u003C/code\u003E.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003ETag helpers are not resolved in theme views\u003C/td\u003E\u003Ctd\u003ENo \u003Ccode\u003E_ViewImports.cshtml\u003C/code\u003E in the theme folder.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003EWidgets disappear on one page\u003C/td\u003E\u003Ctd\u003EA widget zone was dropped from a copied view.\u003C/td\u003E\u003C/tr\u003E\n\u003Ctr\u003E\u003Ctd\u003ETheme CSS returns 404\u003C/td\u003E\u003Ctd\u003E\u003Ccode\u003EContent/**\u003C/code\u003E is not copied to the output.\u003C/td\u003E\u003C/tr\u003E\n\u003C/tbody\u003E\n\u003C/table\u003E\n\n\u003Ch2 id=\u0022related\u0022\u003ERelated\u003C/h2\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-writing-a-plugin\u0022\u003EWriting a plugin\u003C/a\u003E\u003C/li\u003E\n\u003Cli\u003E\u003Ca href=\u0022/developers-plugin-provider-types\u0022\u003EPlugin provider types\u003C/a\u003E \u2014 widgets add markup to zones without a theme.\u003C/li\u003E\n\u003C/ul\u003E","ParentCategoryId":"6abdec6d83d2816248229f65","SeName":"developers-creating-a-theme","MetaKeywords":null,"MetaDescription":"Create a GrandNode storefront theme: IThemeView, view overrides with fallback to the default views, _ViewImports, content assets and theme selection.","MetaTitle":null,"AllowComments":false,"Captcha":{"ReCaptchaChallengeField":null,"ReCaptchaResponseField":null,"ReCaptchaResponseValue":null,"ReCaptchaResponse":null},"RelatedArticles":[],"CategoryBreadcrumb":[{"Name":"For developers","Description":null,"IsCurrent":false,"Children":null,"Parent":null,"SeName":"docs-developers","Id":"6abdec6d83d2816248229f65","UserFields":[]}],"AddNewComment":{"CommentText":null,"DisplayCaptcha":false,"Id":null,"UserFields":[]},"Comments":[],"Id":"6abdec6d83d2816248229f6f","UserFields":[]}