@alphasquad/saleor-template-advance 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/next.config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { NextConfig } from "next";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import fs from "node:fs";
4
5
 
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7
 
@@ -47,12 +48,49 @@ function getRemoteImageHosts(): string[] {
47
48
  );
48
49
  }
49
50
 
51
+ // --- Tenant Override Resolution ---
52
+ // When running via @alphasquad/create-saleor-storefront CLI wrapper,
53
+ // process.cwd() is the tenant root. The tenant can place override components
54
+ // in src/overrides/ which the template will use instead of its defaults.
55
+ const tenantRoot = process.cwd();
56
+ const tenantOverridesIndex = path.join(tenantRoot, "src", "overrides", "index.ts");
57
+ const tenantOverridesDir = path.join(tenantRoot, "src", "overrides");
58
+ const hasTenantOverrides =
59
+ fs.existsSync(tenantOverridesIndex) ||
60
+ fs.existsSync(tenantOverridesIndex.replace(".ts", ".tsx")) ||
61
+ fs.existsSync(tenantOverridesIndex.replace(".ts", ".js"));
62
+
50
63
  const nextConfig: NextConfig = {
51
64
  // Next.js skips tsconfig path resolution for files inside node_modules.
52
65
  // When this template is consumed as a package, we need explicit aliases.
53
66
  transpilePackages: ["@alphasquad/saleor-template-advance"],
54
67
  webpack(config) {
55
68
  config.resolve.alias["@"] = path.resolve(__dirname, "src");
69
+
70
+ // Map @tenant-overrides to tenant's src/overrides if it exists,
71
+ // otherwise use the template's empty defaults
72
+ config.resolve = config.resolve || {};
73
+ config.resolve.alias = config.resolve.alias || {};
74
+
75
+ if (hasTenantOverrides) {
76
+ (config.resolve.alias as Record<string, string>)["@tenant-overrides"] =
77
+ tenantOverridesDir;
78
+ } else {
79
+ // When no tenant overrides exist, use the template's empty defaults.
80
+ // In Next.js, next.config.ts is compiled and __dirname is available.
81
+ // However when installed as npm package, we need the template package root.
82
+ // The config file is always at the template root, so we use a simple heuristic:
83
+ // try the template root (where next.config.ts lives) first, then cwd.
84
+ const candidates = [
85
+ path.join(path.dirname(require.resolve("./package.json")), "src", "lib", "overrides", "defaults"),
86
+ path.join(process.cwd(), "src", "lib", "overrides", "defaults"),
87
+ ];
88
+ const defaultsPath = candidates.find((p) =>
89
+ fs.existsSync(p + ".ts") || fs.existsSync(p + ".js")
90
+ ) || candidates[0];
91
+ (config.resolve.alias as Record<string, string>)["@tenant-overrides"] = defaultsPath;
92
+ }
93
+
56
94
  return config;
57
95
  },
58
96
  eslint: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alphasquad/saleor-template-advance",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "scripts": {
5
5
  "dev": "next dev --turbopack",
6
6
  "build": "next build",
@@ -1,3 +1,4 @@
1
+ import { storefrontOverrides } from "@tenant-overrides";
1
2
  import { fetchMenuBySlug } from "@/graphql/queries/getMenuBySlug";
2
3
  import Image from "next/image";
3
4
  import Link from "next/link";
@@ -26,6 +27,15 @@ type FooterSection = {
26
27
  children: FooterChild[];
27
28
  };
28
29
 
30
+ /**
31
+ * Props passed to both the default and any tenant override footer.
32
+ * Server-fetched data is always provided, preserving SSR performance.
33
+ */
34
+ export interface FooterRendererProps {
35
+ footerMenu: any;
36
+ siteInfo: any;
37
+ }
38
+
29
39
  const SectionTitle = ({ children }: { children: React.ReactNode }) => (
30
40
  <span className="text-base font-secondary font-bold text-white uppercase tracking-wider">
31
41
  {children}
@@ -83,22 +93,17 @@ function normalizePhone(raw: string) {
83
93
  return digits ? `tel:${digits}` : null;
84
94
  }
85
95
 
86
- const Footer = async () => {
87
- const currentYear = new Date().getFullYear();
96
+ const DefaultFooterRenderer = ({ footerMenu, siteInfo }: FooterRendererProps) => {
97
+ const currentYear = new Date().getFullYear();
88
98
 
89
99
  const TENANT_NAME = process.env.NEXT_PUBLIC_BRAND_NAME || process.env.NEXT_PUBLIC_TENANT_NAME || "Storefront";
90
100
  const logo =
91
101
  process.env.NEXT_PUBLIC_LOGO_URL ||
92
102
  "https://webshopmanager.com/files/images/logo.png";
93
103
 
94
- // Fetch footer menu data from backend
95
- const footerMenu = await fetchMenuBySlug("footer");
96
-
97
- // Fetch site info for address and phone
98
- const siteInfo = await fetchSiteInfo();
99
104
  const getInfo = (k: string) =>
100
105
  siteInfo?.metadata
101
- ?.find((m) => m.key.toLowerCase() === k.toLowerCase())
106
+ ?.find((m: any) => m.key.toLowerCase() === k.toLowerCase())
102
107
  ?.value?.trim() || "";
103
108
 
104
109
  const address = getInfo("Address");
@@ -280,4 +285,23 @@ function normalizePhone(raw: string) {
280
285
  );
281
286
  };
282
287
 
288
+ /**
289
+ * Server component that fetches footer data and delegates rendering
290
+ * to either the tenant's override or the default footer.
291
+ *
292
+ * Data fetching always happens server-side regardless of which renderer is used,
293
+ * preserving SSR performance and SEO.
294
+ */
295
+ async function Footer() {
296
+ const [footerMenu, siteInfo] = await Promise.all([
297
+ fetchMenuBySlug("footer"),
298
+ fetchSiteInfo(),
299
+ ]);
300
+
301
+ const FooterRenderer =
302
+ (storefrontOverrides as any).Footer || DefaultFooterRenderer;
303
+
304
+ return <FooterRenderer footerMenu={footerMenu} siteInfo={siteInfo} />;
305
+ }
306
+
283
307
  export default Footer;
@@ -2,17 +2,23 @@ import { Suspense } from "react";
2
2
  import { NavBar } from "./navBar";
3
3
  import TopBar from "./topBar";
4
4
  import { fetchCategories, fetchMenuData } from "@/hooks/serverNavbarData";
5
+ import type { CategoryNode, MenuItem } from "@/hooks/serverNavbarData";
6
+ import { storefrontOverrides } from "@tenant-overrides";
5
7
 
6
- export const Header = async () => {
7
- const [categories, menuItems] = await Promise.allSettled([
8
- fetchCategories(),
9
- fetchMenuData(),
10
- ]);
11
-
12
- const categoriesData =
13
- categories.status === "fulfilled" ? categories.value : [];
14
- const menuItemsData = menuItems.status === "fulfilled" ? menuItems.value : [];
8
+ /**
9
+ * Props passed to both the default and any tenant override header.
10
+ * This ensures tenant headers receive server-fetched data without
11
+ * needing client-side fetches (preserving SSR performance).
12
+ */
13
+ export interface HeaderRendererProps {
14
+ categories: CategoryNode[];
15
+ menuItems: MenuItem[];
16
+ }
15
17
 
18
+ const DefaultHeaderRenderer = ({
19
+ categories,
20
+ menuItems,
21
+ }: HeaderRendererProps) => {
16
22
  return (
17
23
  <header className="w-full">
18
24
  <Suspense
@@ -23,7 +29,6 @@ export const Header = async () => {
23
29
  />
24
30
  }
25
31
  >
26
- {/* Contact + Timings Banner */}
27
32
  <TopBar />
28
33
  </Suspense>
29
34
  <Suspense
@@ -37,8 +42,33 @@ export const Header = async () => {
37
42
  />
38
43
  }
39
44
  >
40
- <NavBar categories={categoriesData} menuItems={menuItemsData} />
45
+ <NavBar categories={categories} menuItems={menuItems} />
41
46
  </Suspense>
42
47
  </header>
43
48
  );
44
49
  };
50
+
51
+ /**
52
+ * Server component that fetches navigation data and delegates rendering
53
+ * to either the tenant's override or the default header.
54
+ *
55
+ * Data fetching always happens server-side regardless of which renderer is used,
56
+ * preserving SSR performance and SEO (no client-side fetch waterfalls).
57
+ */
58
+ export const Header = async () => {
59
+ const [categories, menuItems] = await Promise.allSettled([
60
+ fetchCategories(),
61
+ fetchMenuData(),
62
+ ]);
63
+
64
+ const categoriesData =
65
+ categories.status === "fulfilled" ? categories.value : [];
66
+ const menuItemsData = menuItems.status === "fulfilled" ? menuItems.value : [];
67
+
68
+ const HeaderRenderer =
69
+ (storefrontOverrides as any).Header || DefaultHeaderRenderer;
70
+
71
+ return (
72
+ <HeaderRenderer categories={categoriesData} menuItems={menuItemsData} />
73
+ );
74
+ };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { storefrontOverrides } from "@tenant-overrides";
3
4
  import { GET_PAGE_METADATA_BY_SLUG } from "@/graphql/queries/getHeroMetadata";
4
5
  import { useQuery } from "@apollo/client";
5
6
  import Image from "next/image";
@@ -62,7 +63,7 @@ const SearchFormSection = () => {
62
63
  );
63
64
  };
64
65
 
65
- export const ShowroomHeroCarousel = ({}: ShowroomHeroCarouselProps) => {
66
+ const DefaultShowroomHeroCarousel = ({}: ShowroomHeroCarouselProps) => {
66
67
  const { data, loading } = useQuery(GET_PAGE_METADATA_BY_SLUG, {
67
68
  variables: { slug: "hero-section" },
68
69
  });
@@ -139,3 +140,8 @@ export const ShowroomHeroCarousel = ({}: ShowroomHeroCarouselProps) => {
139
140
  </div>
140
141
  );
141
142
  };
143
+
144
+ // Allow tenant repos to override the hero carousel via storefrontOverrides.ShowroomHeroCarousel
145
+ export const ShowroomHeroCarousel =
146
+ (storefrontOverrides as any).ShowroomHeroCarousel ||
147
+ DefaultShowroomHeroCarousel;
@@ -1,10 +1,11 @@
1
+ import { storefrontOverrides } from "@tenant-overrides";
1
2
  import { GET_TESTIMONIAL_PAGE_TYPE } from "@/graphql/queries/getPageTypeId";
2
3
  import { GET_TESTIMONIALS } from "@/graphql/queries/getTestimonials";
3
4
  import createApolloServerClient from "@/graphql/server-client";
4
5
  import Heading from "../reuseableUI/heading";
5
6
  import { TestimonialCard } from "../reuseableUI/testimonialCard";
6
7
 
7
- export const TestimonialsGrid = async ({ first = 6 }: { first?: number }) => {
8
+ const DefaultTestimonialsGrid = async ({ first = 6 }: { first?: number }) => {
8
9
  let items: Array<{
9
10
  id: string;
10
11
  name: string;
@@ -104,3 +105,7 @@ export const TestimonialsGrid = async ({ first = 6 }: { first?: number }) => {
104
105
  </section>
105
106
  );
106
107
  };
108
+
109
+ // Allow tenant repos to override via storefrontOverrides.TestimonialsGrid
110
+ export const TestimonialsGrid =
111
+ (storefrontOverrides as any).TestimonialsGrid || DefaultTestimonialsGrid;
package/src/app/page.tsx CHANGED
@@ -9,6 +9,7 @@ import { getStoreName } from "./utils/branding";
9
9
  import { BrandsSwiperServer } from "./components/showroom/brandsSwiperServer";
10
10
  import NewslettersHomeModal from "./components/reuseableUI/newsletter/newslettersHomeModal";
11
11
  import { FeaturesStrip } from "./components/showroom/featureStrip";
12
+ import { withOverride } from "@/lib/overrides/resolve";
12
13
 
13
14
  export const metadata: Metadata = {
14
15
  title: `Home - ${getStoreName()}`,
@@ -73,7 +74,7 @@ const FeaturedBrandsLoadingState = () => {
73
74
  );
74
75
  };
75
76
 
76
- export default function Home() {
77
+ function DefaultHome() {
77
78
  return (
78
79
  <main className="overflow-x-hidden">
79
80
  <Suspense fallback={<SkeletonLoader type="hero" />}>
@@ -192,3 +193,7 @@ export default function Home() {
192
193
  </main>
193
194
  );
194
195
  }
196
+
197
+ // Allow tenant repos to override the entire homepage via storefrontOverrides.HomePage
198
+ const HomePage = withOverride("HomePage", DefaultHome);
199
+ export default HomePage;
@@ -0,0 +1,80 @@
1
+ # Tenant Override System
2
+
3
+ This template supports component overrides from tenant wrapper repos created by `@alphasquad/create-saleor-storefront`.
4
+
5
+ ## How It Works
6
+
7
+ 1. The CLI creates a tenant repo with `src/overrides/index.ts` that exports a `storefrontOverrides` registry
8
+ 2. `next.config.ts` maps `@tenant-overrides` to the tenant's `src/overrides/` directory (or empty defaults if none exist)
9
+ 3. Template components check the registry and use tenant overrides when available, falling back to template defaults
10
+
11
+ ## SSR & Performance Guarantees
12
+
13
+ The override system is designed to preserve server-side rendering and performance:
14
+
15
+ - **Header**: The template always fetches `categories` and `menuItems` server-side. Both the default and any tenant override receive these as props (`HeaderRendererProps`). Tenant headers do NOT need to fetch navigation data client-side.
16
+ - **Footer**: The template always fetches `footerMenu` and `siteInfo` server-side. Both the default and any tenant override receive these as props (`FooterRendererProps`).
17
+ - **ShowroomHeroCarousel**: Client component. Override replaces it 1:1.
18
+ - **TestimonialsGrid**: Server component. Override replaces it 1:1.
19
+ - **HomePage**: Full page replacement. The template's metadata export still applies.
20
+
21
+ **Rule**: If the template component is a server component that fetches data, the override system passes that data as props. Tenant overrides should NOT re-fetch the same data client-side.
22
+
23
+ ## Supported Override Keys
24
+
25
+ | Key | Props Received | Rendering |
26
+ |-----|---------------|-----------|
27
+ | `HomePage` | None (full page) | Server or Client |
28
+ | `Header` | `{ categories, menuItems }` | Server-fetched, render only |
29
+ | `Footer` | `{ footerMenu, siteInfo }` | Server-fetched, render only |
30
+ | `ShowroomHeroCarousel` | None | Client component |
31
+ | `TestimonialsGrid` | `{ first }` | Server component |
32
+
33
+ ## Tenant Override Example
34
+
35
+ ```ts
36
+ // src/overrides/index.ts
37
+ import MyHomePage from "./HomePage";
38
+ import MyHeader from "./Header";
39
+
40
+ export const storefrontOverrides = {
41
+ HomePage: MyHomePage,
42
+ Header: MyHeader,
43
+ };
44
+ ```
45
+
46
+ ```tsx
47
+ // src/overrides/Header.tsx
48
+ // Receives server-fetched data as props - no client-side fetching needed
49
+ export default function MyHeader({ categories, menuItems }) {
50
+ return (
51
+ <header>
52
+ <nav>
53
+ {menuItems.map(item => (
54
+ <a key={item.id} href={item.url}>{item.name}</a>
55
+ ))}
56
+ </nav>
57
+ </header>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ## Adding New Override Points
63
+
64
+ To make a new component overridable while preserving SSR:
65
+
66
+ 1. Import: `import { storefrontOverrides } from "@tenant-overrides";`
67
+ 2. If the component fetches data server-side:
68
+ - Define a `RendererProps` interface with the fetched data
69
+ - Split into: async data-fetcher (stays as export) + renderer (overridable)
70
+ - The data-fetcher passes props to whichever renderer is active
71
+ 3. If the component is client-only:
72
+ - Rename: `const DefaultFoo = ...`
73
+ - Export with override: `export const Foo = (storefrontOverrides as any).Foo || DefaultFoo;`
74
+
75
+ ## Zero Impact When Unused
76
+
77
+ When no tenant overrides exist (e.g., running the template directly):
78
+ - `@tenant-overrides` resolves to `src/lib/overrides/defaults.ts` (empty object)
79
+ - All override checks resolve to `DefaultXxx` immediately
80
+ - No bundle size increase, no runtime overhead
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default (empty) overrides registry.
3
+ * Used as fallback when no tenant overrides are found.
4
+ */
5
+ export const storefrontOverrides: Record<string, never> = {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Override Resolution System
3
+ *
4
+ * Allows tenant wrapper repos (created by @alphasquad/create-saleor-storefront)
5
+ * to override specific template components by placing files in src/overrides/.
6
+ *
7
+ * Supported override keys (expand as needed):
8
+ * - HomePage: Replaces the entire homepage content
9
+ * - Header: Replaces the site header
10
+ * - Footer: Replaces the site footer
11
+ * - ShowroomHeroCarousel: Replaces the hero section
12
+ * - FeaturesStrip: Replaces the features strip
13
+ * - CategoryGrid: Replaces the category grid
14
+ * - ProductGrid: Replaces the product grid
15
+ * - TestimonialsGrid: Replaces the testimonials section
16
+ * - AboutUs: Replaces the about us section
17
+ * - BrandsSwiper: Replaces the brands swiper
18
+ */
19
+
20
+ export { storefrontOverrides } from "@tenant-overrides";
21
+
22
+ // Re-export a typed helper
23
+ import { storefrontOverrides } from "@tenant-overrides";
24
+ import type { ComponentType } from "react";
25
+
26
+ /**
27
+ * Resolves an override component by key.
28
+ * Returns the tenant's override if available, otherwise the provided default.
29
+ */
30
+ export function withOverride<P extends Record<string, unknown>>(
31
+ key: string,
32
+ DefaultComponent: ComponentType<P>
33
+ ): ComponentType<P> {
34
+ const Override = (storefrontOverrides as Record<string, ComponentType<any>>)[
35
+ key
36
+ ];
37
+ return Override || DefaultComponent;
38
+ }
@@ -0,0 +1,4 @@
1
+ declare module "@tenant-overrides" {
2
+ import type { ComponentType } from "react";
3
+ export const storefrontOverrides: Record<string, ComponentType<any>>;
4
+ }