diff --git a/.ai/skills/theme-creation.md b/.ai/skills/theme-creation.md
index 64b662ce93..c7cda45d16 100644
--- a/.ai/skills/theme-creation.md
+++ b/.ai/skills/theme-creation.md
@@ -84,7 +84,8 @@ Do not use this skill for changes to `Grand.Web/Views` themselves — that is `.
16. Add `Views/{ThemeName}/_ViewStart.cshtml`.
17. Copy **only** the views the theme actually changes. Everything else resolves through the fallback locations. Each copied view is a file that must be re-reconciled on every upgrade.
18. A copied view keeps the same `@model`, the same route names, and the same storefront data attributes — `data-cart-action`, product IDs, quick-view URLs, wishlist, compare, add-to-cart, image `alt` / `title` / `loading`.
-19. Keep widget zones (`@await Component.InvokeAsync("Widget", new { widgetZone = "..." })`) present in copied views. Dropping a zone silently disables every installed widget on that page.
+19. Keep widget zones (`` or `@await Component.InvokeAsync("Widget", new { widgetZone = "..." })`) present in copied views. Dropping a zone silently disables every installed widget on that page.
+ A `` tag only works for a view component in an assembly the theme registers with `@addTagHelper`, which here is `Grand.Web.Common` (``). A theme cannot reference `Grand.Web` (`GRAND001`), so every other `` copied from a `Grand.Web` view, such as ``, renders as an empty unknown element with no build error. Rewrite those as `@await Component.InvokeAsync("RelatedProducts", new { productId = Model.Id })`.
20. A theme must not change view models, controller actions, or route definitions. If a theme needs different data, the change belongs in `Grand.Web`, not the theme.
#### Assets
diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs b/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs
index 7b6a30d16c..e83df6ba9e 100644
--- a/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs
+++ b/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs
@@ -217,4 +217,18 @@ public static partial class CacheKey
public static string PAGES_PATTERN_KEY => "Grand.pages.";
#endregion
+
+ #region Widgets
+
+ ///
+ /// Key for widget info
+ ///
+ ///
+ /// {0} : current store ID
+ /// {1} : customer groups
+ /// {2} : widget zone
+ ///
+ public static string WIDGET_MODEL_KEY => "Grand.pres.widget-{0}-{1}-{2}";
+
+ #endregion
}
\ No newline at end of file
diff --git a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml
index ef3f289560..720dba9391 100644
--- a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml
+++ b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml
@@ -236,10 +236,10 @@
-
-
-
-
+ @await Component.InvokeAsync("ProductsAlsoPurchased", new { productId = Model.Id })
+ @await Component.InvokeAsync("RelatedProducts", new { productId = Model.Id })
+ @await Component.InvokeAsync("SimilarProducts", new { productId = Model.Id })
+ @await Component.InvokeAsync("RecommendedProducts", new { productId = Model.Id })
@if (Model.RenderCaptcha)
{
diff --git a/src/Tests/Grand.Web.Tests/Components/WidgetViewComponentTests.cs b/src/Tests/Grand.Web.Common.Tests/Components/WidgetViewComponentTests.cs
similarity index 97%
rename from src/Tests/Grand.Web.Tests/Components/WidgetViewComponentTests.cs
rename to src/Tests/Grand.Web.Common.Tests/Components/WidgetViewComponentTests.cs
index d2ba6c715e..2794a0b3d0 100644
--- a/src/Tests/Grand.Web.Tests/Components/WidgetViewComponentTests.cs
+++ b/src/Tests/Grand.Web.Common.Tests/Components/WidgetViewComponentTests.cs
@@ -4,14 +4,14 @@
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Configuration;
using Grand.Mediator;
-using Grand.Web.Components;
+using Grand.Web.Common.Components;
using Grand.Web.Models.Cms;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
-namespace Grand.Web.Tests.Components;
+namespace Grand.Web.Common.Tests.Components;
[TestClass]
public class WidgetViewComponentTests
diff --git a/src/Web/Grand.Web/Components/Widget.cs b/src/Web/Grand.Web.Common/Components/WidgetViewComponent.cs
similarity index 90%
rename from src/Web/Grand.Web/Components/Widget.cs
rename to src/Web/Grand.Web.Common/Components/WidgetViewComponent.cs
index 8cd3d58417..91f9a3a455 100644
--- a/src/Web/Grand.Web/Components/Widget.cs
+++ b/src/Web/Grand.Web.Common/Components/WidgetViewComponent.cs
@@ -2,12 +2,11 @@
using Grand.Domain.Customers;
using Grand.Infrastructure;
using Grand.Infrastructure.Caching;
-using Grand.Web.Common.Components;
-using Grand.Web.Events.Cache;
+using Grand.Infrastructure.Caching.Constants;
using Grand.Web.Models.Cms;
using Microsoft.AspNetCore.Mvc;
-namespace Grand.Web.Components;
+namespace Grand.Web.Common.Components;
public class WidgetViewComponent : BaseViewComponent
{
@@ -24,7 +23,7 @@ public WidgetViewComponent(IContextAccessor contextAccessor, ICacheBase cacheBas
public async Task InvokeAsync(string widgetZone, object additionalData = null)
{
- var cacheKey = string.Format(CacheKeyConst.WIDGET_MODEL_KEY, _contextAccessor.StoreContext.CurrentStore.Id,
+ var cacheKey = string.Format(CacheKey.WIDGET_MODEL_KEY, _contextAccessor.StoreContext.CurrentStore.Id,
string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()),
widgetZone);
diff --git a/src/Web/Grand.Web.Models/Cms/WidgetModel.cs b/src/Web/Grand.Web.Common/Models/Cms/WidgetModel.cs
similarity index 73%
rename from src/Web/Grand.Web.Models/Cms/WidgetModel.cs
rename to src/Web/Grand.Web.Common/Models/Cms/WidgetModel.cs
index 0dcccd91cf..318b4debba 100644
--- a/src/Web/Grand.Web.Models/Cms/WidgetModel.cs
+++ b/src/Web/Grand.Web.Common/Models/Cms/WidgetModel.cs
@@ -1,5 +1,6 @@
using Grand.Infrastructure.Models;
+//the namespace stays from Grand.Web.Models: storefront and theme _ViewImports import it
namespace Grand.Web.Models.Cms;
public class WidgetModel : BaseModel
@@ -7,4 +8,4 @@ public class WidgetModel : BaseModel
public string WidgetZone { get; set; }
public string ViewComponentName { get; set; }
public object AdditionalData { get; set; }
-}
\ No newline at end of file
+}
diff --git a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs
index 7752ef63a2..b72d36efb6 100644
--- a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs
+++ b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs
@@ -288,16 +288,6 @@ public static class CacheKeyConst
///
public const string SITEMAP_PAGE_MODEL_KEY = "Grand.pres.sitemap.page-{0}-{1}-{2}";
- ///
- /// Key for widget info
- ///
- ///
- /// {0} : current store ID
- /// {1} : customer groups
- /// {2} : widget zone
- ///
- public const string WIDGET_MODEL_KEY = "Grand.pres.widget-{0}-{1}-{2}";
-
///
/// Key for caching
///