docs
  1. Storefront Application
  2. Best Practices
  3. Multi-brand Support

Multi-Brand Support

SCAYLE Storefront Application does not include multi-brand support out-of-the-box. For certain scenarios, it is, however, possible to extend the Storefront Application with a basic multi-brand approach.

This guide provides a conceptual approach for implementing basic multi-brand configuration and theming capabilities within a Storefront Application. This multi-brand approach allows you to run different brands with unique configurations, basic theming, and country shop settings from a single codebase by focusing on build-time configuration.

This guide does not provide a complete implementation or full code.

A basic multi-brand system might consist of several key components:

  1. Brand Resolver: Build-time brand resolution and central brand registry
    1. (config/utils/brandResolver.ts ).
  2. Individual Brand Configurations: Individual brand definitions with shops and theming
    1. (config/brands/*.brandConfig.ts).
  3. Brand-Specific Components: Component implementations that vary by brand.
  4. Build-Time Constant: A constant injected at build time for conditional component rendering.
  5. Theme Integration: Automatic Tailwind CSS theming per brand.
  6. Type Definitions: TypeScript interfaces to ensure type safety during built-time
    1. (config/utils/storefront.d.ts).

Configuration Structure

By default, Storefront Application ships with a single brand, multiple-country shops approach. The country shop-specific configuration resides within a dedicated config file (config/shops.ts), which is imported and used within the nuxt.config.ts at build-time to create the final build output.

The architecture of the Storefront Application does not allow multiple brands, each with its own specific country shops, to be served from a single Storefront Application build. Each brand will need to have its own Storefront Application built.

Over the course of this guide, we will implement a structure that creates, modifies or deletes the following files and directories in a Storefront Application project:

The following outlined approach provides the following:

  • Individual brand files: Each brand has its own .brandConfig.ts file containing both shop configurations and theming.
  • Central registry: The brandResolver.ts file imports and registers all brand configurations.
  • Build-time resolution: Brand selection happens at build time via SHOP_BRAND_ID environment variable.
  • Type safety: All configurations use strongly typed TypeScript interfaces.
  • Brand Shop Key Constant Injection: Allowing to use the shop key of a brand for conditional code blocks to enable tree-shaking.
  • Component organization: Brand-specific components are stored alongside default components with descriptive naming and dedicated prefix.

The approach concentrates on the following integration points:

  • Tailwind CSS: Automatic theme application with fallback values for brands without custom theming.
  • Nuxt configuration: Brand name exposed in runtime config, build-time constant injection for components.
  • Component system: Brand-specific components selected via build-time constant for conditional rendering.
  • Build optimization: Tree-shaking ensures only active brand components are included in the bundle.

1. TypeScript Interface Definitions

Our multi-brand system should use strongly typed interfaces to ensure type safety throughout the development and configuration process. These can be defined in config/utils/storefront.d.ts:

2. Brand Resolver and Configuration Registry

If we want to distinguish between brands and load the correct shop configurations for each chosen brand at build-time, we need a way to identify and resolve the brand shop key during the build process of the Storefront Application.

This can be achieved by using a brand resolver. It acts as the central registry, imports individual brand configurations and provides factory functions that return the appropriate brand-specific shop configurations via an environment variable SHOP_KEY at build-time. This allows the brand configuration to be "baked" into the application bundle and cannot be changed without rebuilding the application.

Brand configuration via brandResolver functions is only available during build-time and should never be accessed at runtime. The brandResolver functions are designed exclusively for build-time tooling such as Nuxt configuration and Tailwind CSS setup.

Never import or use brandResolver functions in components, composables, or any runtime code. Brand configuration is baked into the build and should be accessed through other means at runtime (e.g., runtime config, CSS variables).

3. Individual Brand Configuration Files

As we now need to handle multiple brand, each with its own custom country shop configuration, we will use dedicated configuration files per brand. This configuration file will replace the existing config/shops.ts config and will contain the individual shop configuration per brand, as well as additional brand-specific configuration values.

Additional brand-specific configuration values include brand name, brand id (shop key) and theming values.

  • Example Brand Configuration "Brand A":
    &#xNAN;(No brand-specific theming, fallback to values defined in tailwind.config.ts.)
  • Example Brand Configuration "Brand B":
    &#xNAN;(Custom theming values that will be passed to tailwind.config.ts at build-time.)

Have in mind that each country shop within a brand configuration uses the StorefrontShopAndLocaleConfig interface while defining your custom brand configuration:

4. Integration into Tailwind

Using the brand resolver, brand-specific themes can automatically be applied to Tailwind CSS during build. The configuration uses fallback values for brands without a custom theme object. The existing tailwind.config.ts need to be extended to resolve the current brand config and assign either the brand theme values or the default fallback values:

5. Integration into Nuxt Configuration

As the multi-brand approach influences the build behavior of Nuxt, we need to extend the nuxt.config.ts to load the brand-specific configuration and pass it to the necessary

  • i18n locales generated from shop configurations
  • Shop routing based on locale codes and domains
  • Runtime configuration with brand-specific settings (e.g., shopName)

By replacing the existing import of shops (configuration for country shops) with assigning getCurrentBrandConfig().shops to a new shops constant, we can keep the required changes to a minimum. As the shops constant is used throughout the Storefront Application-specific Nuxt configuration, the brand-specific country shop configuration is used and applied, e.g. for configuring the appropriate country shops and related i18n purposes.

This approach ensures...

  • the brand name is available at runtime through the public runtime configuration.
  • build-time constants are available in both client and server code.
  • country shop configurations are properly integrated with i18n settings and routing.

6. Build-time Constant Injection

The brand configuration should by now be successfully integrated into the overall Nuxt and Tailwind configuration. We now need a way to decide which code to include and execute for a specific brand. This can be useful to have slightly different code behavior per brand or to import and render a different component.

To enable conditional selection at build-time, we can inject build-time constants that gets replaced during compilation with their actual string values, allowing unused code to be tree-shaken during the build process and thus avoiding shipping dead code for a brand deployment.

This approach ensures that...

  • the constant is replaced with the actual brand ID string during compilation.
  • unused brand components are tree-shaken from the final bundle.
  • the brand ID is available in both client and server code.
  • no runtime brand resolution occurs, maintaining optimal performance.

Consider that three-shaking using injected build-time constant is only handled for processed TypeScript (e.g. in TypeScript files or in Vue component script tags). It might not work for Vue component templates.

Each new build-time constant needs to be defined in three places:

  • Nuxt Configuration (for client-side):
    &#xNAN;For the final client-side bundle output we want to inject the build-time constant for tree-shaking to take effect.

    &#xNAN;We also need to make sure that we always stringify the value we want to pass into the injected constant.
  • Nitro Configuration (for server-side):
    &#xNAN;As the Storefront Application relies on server-side rendering, we need to make sure that the injected constant is not only available at build-time for client-side tree-shaking, but also that its properly injected for the server-side rendering environment via the underlying Nitro server.

    &#xNAN;We also need to make sure that we always stringify the value we want to pass into the injected constant.
  • TypeScript Declaration:
    &#xNAN;If we want to rely on our injected build-time variables in our TypeScript files or Vue script tags, we need to declare these constants so that TypeScript is aware of its existence. This allows TypeScript to recognize the constant and enables proper type checking, avoiding any potential type issues during development and build.

7. Testing & Building Different Brands

Now that we have the dedicated brand configurations and a way to reliably resolve the brand at build-time, we need to pass these values to the Storefront Application build process.

We can set the active brand using the SHOP_KEY environment variable during build:

The brand must be specified at build-time. Runtime brand switching is not supported.

Each brand requires a separate build and deployment.

For testing purposes of the production build output, we can simply run pnpm preview. As the passed SHOP_KEY will be used and evaluated during the build process, we don't need to pass it again if we want to execute pnpm preview.

During development, changing the SHOP_KEY environment variable requires restarting the development server. Multi-brand configuration is resolved at startup and cannot be changed at runtime.

Development

  1. We create a brand configuration file:
  2. We define a new brand configuration file(see "3. Individual Brand Configuration Files")
  3. We import and add our new brand configuration into the brand resolver:
  4. If we want to use different behavior within an existing TypeScript file or Vue component, we can use the injected build-time constant __CURRENT_BRAND_SHOP_KEY__ . In the context of a Vue component it is important to remember that we should never use the injected constant directly within the template tag for e.g. a. v-if condition, but need to create a script const first.
    This is important so we can ensure the string value is injected during TypeScript build and that the Vue template compiler is properly able to evaluate and omit unnecessary v-dom nodes during build-time, reducing the client bundle size:
  5. If we have more complex cases where child components have larger internal changes (e.g. template markdup and behavior), we can use Vue dynamic component rendering in combination with our build-time constant. For this, we create an alternative brand-specific component implementation in app/components using descriptive naming to distinguish the component, e.g. using a brand-specific filename prefix like NewBrandComponent.vue in parallel to the existing DefaultComponent.vue:
  6. As we're able to inject custom brand-specific Tailwind value during build-time, Tailwind classes will reflect the configuration changes if properly adopted within the tailwind.config.ts.
    We do not recommend working with brand-specific styling and injected build-time constants, as this will quickly lead to complex and unmaintainable code!
  7. Test new brand via SHOP_KEY=newBrand pnpm dev.

Deployment

Each brand requires a separate build and deployment since brand configuration is determined at build-time by passing the SHOP_KEY environment variable:

Brand switching at runtime is not supported. Each brand deployment must be built with the specific SHOP_KEY environment variable. To serve multiple brands, you need separate builds and deployments for each brand.

Conclusion

This basic multi-brand approach provides a flexible way to manage multiple brands within a single SCAYLE Storefront application when brands share similar layouts and feature sets. By leveraging build-time configuration exclusively, the system ensures optimal performance while providing customization capabilities for theming and shop configurations.