Skip to content

Building Extensions - Developer Guide

This guide provides comprehensive technical documentation for building custom extensions in Badger Commerce. Whether you're creating seasonal effects, integrating third-party services, or enhancing the shopping experience, this guide covers everything you need to know.

Table of Contents

  1. Extension Architecture
  2. Getting Started
  3. Extension Types
  4. Building Your First Extension
  5. Configuration Management
  6. Templates and Resources
  7. Testing Extensions
  8. Deployment
  9. Tutorial: Christmas Mode Extension
  10. Best Practices
  11. Troubleshooting

Extension Architecture

Core Interfaces and Classes

The extension system is built around a hierarchical class structure:

BadgerExtension (interface)
    ↓
BaseBadgerExtension (abstract class)
    ↓
AdministratableBaseBadgerExtension (abstract class)
    ↓
AdministratableBaseBadgerExtensionV2 (abstract class) ← Use this!

BadgerExtension Interface

The root interface defining the contract for all extensions.

Key constants: - TEMPLATE_PATH_KEY - Key for template path in model - PAGE_LOCATION_KEY - Key for page slot location - CSS_LIST_LOCATION_KEY - Key for CSS files list - JS_LIST_LOCATION_KEY - Key for JavaScript files list - PAGE_RENDER_ORDER - Key for display priority

Required methods: - getName() - Unique extension identifier - getDescription() - Human-readable description - getAuthor() - Extension author - getVersion() - Semantic version (e.g., "1.0.0") - isGlobal() - Whether extension applies globally - getSiteId() - Site identifier (use ALL_SITES constant)

BaseBadgerExtension

Provides utility methods for extension development.

Useful methods:

// Create a model map for rendering
Map<String, Object> createModel(
    String pageLocation,
    int displayPriority,
    String templatePath,
    ExtensionConfigurationBean configuration
);

// Add JavaScript file to extension
void requiresJS(Map<String, Object> model, String jsPath);

// Add CSS file to extension
void requiresCSS(Map<String, Object> model, String cssPath);

// Declare that the extension's JS needs Stripe.js on the page
void requiresStripeJS(Map<String, Object> model);

// Find configuration value
<T> Optional<T> findPropertyConfiguration(
    String key,
    ExtensionConfigurationBean configuration,
    Class<T> type
);

AdministratableBaseBadgerExtension (V1 Pattern)

Older pattern that separates admin and view rendering.

Methods to implement: - getDefaultPageLocation() - Default slot for extension - generateViewExtensionModel() - Render for customers - generateAdminExtensionModel() - Render for admin interface

Not recommended for new extensions - use V2 instead.

AdministratableBaseBadgerExtensionV2 (V2 Pattern)

Recommended for all new extensions. Uses declarative configuration with EditableField<?>.

Methods to implement:

@NotNull String getDefaultPageLocation();

@NotNull List<EditableField<?>> getEditableFields();

List<String> getAdminJSPaths();  // Optional

List<String> getAdminCSSPaths();  // Optional

Map<String, Object> generateViewExtensionModel(...);

Map<String, Object> generateViewExtensionModelForFormPost(...);

Extension Service

The ExtensionService is the central registry for all extensions.

Key responsibilities: - Auto-discovers extensions via Spring dependency injection - Manages extension lifecycle - Renders extensions for given contexts - Handles global and item-level extensions

Main entry point:

Map<String, List<Map<String, Object>>> renderExtensions(
    ExtensionConfigurationContainer item,
    SiteContext siteContext,
    User user,
    Order order,
    ExtensionRequestContext requestContext
);

Returns a map of page slots → list of extension models.

Page Slots

Extensions are rendered in specific page slots. Common slots defined in SlotNames:

  • TOP_BANNER - Top of page (breadcrumbs, alerts)
  • MAIN_SECTION - Main content area
  • SIDEBAR - Side panel
  • FOOTER - Bottom of page
  • CHECKOUT_SUMMARY - Order summary in checkout
  • PRODUCT_DETAILS - Product detail section
  • SEARCH_RESULTS - Search/collection results

You can also use custom slot names.


Getting Started

Prerequisites

  • Java 21+
  • Spring Boot 3.3.4+
  • Maven 3.3.9+
  • Understanding of Thymeleaf templates
  • Basic knowledge of JavaScript and CSS

Project Structure

Extensions follow a multi-module structure:

badger-commerce/
├── commerce-core/
│   └── src/main/java/.../extensions/
│       └── seasonal/
│           └── ChristmasModeExtension.java  ← Extension class
│
├── web-mvc/
│   └── src/main/resources/
│       ├── templates/
│       │   ├── bootstrap/extensions/christmasMode/
│       │   │   └── main.html  ← Bootstrap template
│       │   └── nova/extensions/christmasMode/
│       │       └── main.html  ← Nova template
│       │
│       └── resources/settBuilder/themes/
│           ├── bootstrap/
│           │   ├── css/extensions/christmasMode/
│           │   │   └── christmas.css
│           │   └── js/extensions/christmasMode/
│           │       └── christmas.js
│           │
│           └── nova/
│               ├── css/extensions/christmasMode/
│               │   └── christmas.css
│               └── js/extensions/christmasMode/
│                   └── christmas.js

Development Workflow

  1. Create Extension Class in commerce-core/src/main/java/.../extensions/
  2. Implement Required Methods using V2 pattern
  3. Add Extension Name to messages.properties resource bundle
  4. Create Templates for each theme in web-mvc/src/main/resources/templates/{theme}/extensions/
  5. Add CSS/JS Resources in web-mvc/src/main/resources/resources/settBuilder/themes/{theme}/
  6. Build Project: mvn clean package -DskipTests
  7. Start Dev Environment: ./dev-scripts/devEnv.sh
  8. Test Extension via admin interface
  9. Iterate with hot reloading support

Extension Types

By Functionality

Content Extensions

Display marketing content, carousels, videos, and rich media.

Examples: Static Carousel, HTML Fragment, YouTube Video

Typical slots: TOP_BANNER, MAIN_SECTION

Product Extensions

Enhance product display with galleries, attributes, and bundles.

Examples: Product Gallery, Size Guide, Bundle Builder

Typical slots: PRODUCT_DETAILS, SIDEBAR

Checkout Extensions

Add functionality to the checkout flow.

Examples: Apple Pay, Address Validation, Gift Options

Typical slots: CHECKOUT_SUMMARY, PAYMENT_OPTIONS

Social Extensions

Reviews, ratings, social sharing, and user-generated content.

Examples: Product Reviews, Social Links, Trending Products

Typical slots: PRODUCT_DETAILS, MAIN_SECTION

Seasonal Extensions

Time-based effects for holidays and special events.

Examples: Christmas Mode, Valentine's Day, Black Friday

Typical slots: TOP_BANNER (global effects)

By Scope

Global Extensions

Applied to all pages automatically.

@Override
public boolean isGlobal() {
    return true;
}

Use cases: Site-wide effects, analytics, chat widgets

Item-Level Extensions

Added to specific pages, products, or collections.

@Override
public boolean isGlobal() {
    return false;
}

Use cases: Product-specific features, page-specific content


Building Your First Extension

Minimal Extension Example

Let's build a simple "Alert Banner" extension.

Step 1: Create Extension Class

commerce-core/src/main/java/.../extensions/content/AlertBannerExtension.java

Important: After creating the extension class, you must also add its display name to the resource bundle (see Step 5).

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */
package uk.co.kedos.badger.settbuilder.extensions.content;

import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import uk.co.kedos.badger.settbuilder.catalogue.*;
import uk.co.kedos.badger.settbuilder.extensions.*;
import uk.co.kedos.badger.settbuilder.extensions.fields.*;
import uk.co.kedos.badger.settbuilder.extensions.slots.SlotNames;
import uk.co.kedos.badger.settbuilder.extensions.v2.AdministratableBaseBadgerExtensionV2;

@Component
public class AlertBannerExtension extends AdministratableBaseBadgerExtensionV2 {

    public AlertBannerExtension(ExtensionUtils extensionUtils) {
        super(extensionUtils);
    }

    @Override
    public @NotNull String getDefaultPageLocation() {
        return SlotNames.TOP_BANNER;
    }

    @Override
    public @NotNull List<EditableField<?>> getEditableFields() {
        return List.of(
            new SingleValueEditableField<>(
                "message",
                "Alert Message",
                "Important announcement!",
                String.class,
                "Message to display in the banner"
            ),
            new SingleValueEditableField<>(
                "type",
                "Alert Type",
                "info",
                String.class,
                "Type: info, success, warning, error"
            )
        );
    }

    @Override
    public List<String> getAdminJSPaths() {
        return null;  // No admin-specific JS needed
    }

    @Override
    public List<String> getAdminCSSPaths() {
        return null;  // No admin-specific CSS needed
    }

    @Override
    public Map<String, Object> generateViewExtensionModel(
            ExtensionConfigurationContainer itemBeingRendered,
            SiteContext siteContext,
            User user,
            Order order,
            ExtensionConfigurationBean configuration,
            ExtensionRequestContext requestContext,
            String extensionId) {

        Map<String, Object> model = createModel(
            getDefaultPageLocation(),
            DEFAULT_DISPLAY_PRIORITY,
            "extensions/alertBanner/main",
            configuration
        );

        // Extract configuration
        String message = findPropertyConfiguration("message", configuration, String.class)
            .orElse("Important announcement!");
        String type = findPropertyConfiguration("type", configuration, String.class)
            .orElse("info");

        model.put("message", message);
        model.put("alertType", type);

        // Add CSS for styling
        requiresCSS(model, "/extensions/alertBanner/alert.css");

        return model;
    }

    @Override
    public Map<String, Object> generateViewExtensionModelForFormPost(
            ExtensionConfigurationContainer itemBeingRendered,
            SiteContext siteContext,
            User user,
            Order order,
            ExtensionConfigurationBean configuration,
            ExtensionRequestContext requestContext,
            String extensionId) {
        return generateViewExtensionModel(
            itemBeingRendered, siteContext, user, order,
            configuration, requestContext, extensionId
        );
    }

    @Override
    public String getName() {
        return "alertBanner";
    }

    @Override
    public String getDescription() {
        return "Displays a customizable alert banner at the top of pages";
    }

    @Override
    public String getAuthor() {
        return "Kedos Consulting Limited";
    }

    @Override
    public String getVersion() {
        return "1.0.0";
    }

    @Override
    public boolean isGlobal() {
        return false;
    }

    @Override
    public String getSiteId() {
        return ALL_SITES;
    }
}

Step 2: Create Templates

Bootstrap Template: templates/bootstrap/extensions/alertBanner/main.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<th:block th:fragment="extension(model)">
    <div th:class="'alert alert-' + ${model.alertType}" role="alert">
        <span th:text="${model.message}">Alert message</span>
    </div>
</th:block>
</html>

Nova Template: templates/nova/extensions/alertBanner/main.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<th:block th:fragment="extension(model)">
    <div
        th:class="'alert-banner alert-' + ${model.alertType}"
        role="alert"
        th:style="'padding: var(--space-4); border-radius: var(--radius-md);'"
    >
        <span th:text="${model.message}">Alert message</span>
    </div>
</th:block>
</html>

Step 3: Add CSS

Bootstrap CSS: themes/bootstrap/css/extensions/alertBanner/alert.css

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

.alert {
    padding: 15px;
    margin-bottom: 20px;
    border: 1px solid transparent;
    border-radius: 4px;
}

.alert-info {
    background-color: #d9edf7;
    border-color: #bce8f1;
    color: #31708f;
}

.alert-success {
    background-color: #dff0d8;
    border-color: #d6e9c6;
    color: #3c763d;
}

.alert-warning {
    background-color: #fcf8e3;
    border-color: #faebcc;
    color: #8a6d3b;
}

.alert-error {
    background-color: #f2dede;
    border-color: #ebccd1;
    color: #a94442;
}

Nova CSS: themes/nova/css/extensions/alertBanner/alert.css

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

.alert-banner {
    padding: var(--space-4);
    margin-bottom: var(--space-5);
    border-radius: var(--radius-md);
    font-size: var(--font-size-base);
}

.alert-info {
    background-color: #dbeafe;
    color: #1e40af;
}

.alert-success {
    background-color: #dcfce7;
    color: #166534;
}

.alert-warning {
    background-color: #fef3c7;
    color: #92400e;
}

.alert-error {
    background-color: #fee2e2;
    color: #991b1b;
}

Step 4: Add Extension Name to Resource Bundle

All extensions must have a display name entry in the resource bundle.

File: commerce-core/src/main/resources/messages.properties

Add the following line in alphabetical order:

admin.extensions.alertBanner.name=Alert Banner

Pattern:

admin.extensions.{extensionName}.name=Display Name

Where {extensionName} matches the value returned by your extension's getName() method.

Why this is required: - The admin interface uses this entry to display the extension name - Without it, the extension may show as "undefined" or fail to render - Maintains internationalization support for future localization

Step 5: Build and Test

# Build the project
mvn clean package -DskipTests

# Start dev environment
./dev-scripts/devEnv.sh

# Access admin at https://localhost:9443/admin
# Add "Alert Banner" extension to a page
# Configure message and type
# View the page to see your extension

Configuration Management

Editable Fields

Extensions expose configuration through EditableField<?> objects.

SingleValueEditableField

For simple scalar values.

new SingleValueEditableField<>(
    "fieldName",           // Key for storage
    "Display Label",       // Shown in admin UI
    "default value",       // Default value
    String.class,          // Data type
    "Help text"            // Description (optional)
);

Supported types: - String.class - Integer.class - Boolean.class - Double.class - Long.class

MultiValueEditableField

For lists of values.

new MultiValueEditableField<>(
    "tags",
    "Product Tags",
    List.of("featured", "sale"),
    String.class,
    "Tags to filter products"
);

Example: Complex Configuration

@Override
public @NotNull List<EditableField<?>> getEditableFields() {
    return List.of(
        // Text field
        new SingleValueEditableField<>(
            "title",
            "Title",
            "Trending Now",
            String.class,
            "Heading to display above products"
        ),

        // Number field
        new SingleValueEditableField<>(
            "productCount",
            "Number of Products",
            5,
            Integer.class,
            "How many products to show (1-20)"
        ),

        // Boolean field
        new SingleValueEditableField<>(
            "showPrices",
            "Show Prices",
            true,
            Boolean.class,
            "Display product prices"
        ),

        // List field
        new MultiValueEditableField<>(
            "categories",
            "Categories",
            List.of("electronics", "clothing"),
            String.class,
            "Categories to include"
        )
    );
}

Reading Configuration Values

Use findPropertyConfiguration() to read values in generateViewExtensionModel():

// With default fallback
String title = findPropertyConfiguration("title", configuration, String.class)
    .orElse("Default Title");

Integer count = findPropertyConfiguration("productCount", configuration, Integer.class)
    .orElse(5);

Boolean showPrices = findPropertyConfiguration("showPrices", configuration, Boolean.class)
    .orElse(true);

// Handle missing values
Optional<String> optionalValue = findPropertyConfiguration(
    "optionalField",
    configuration,
    String.class
);

if (optionalValue.isPresent()) {
    // Use value
}

Configuration Validation

Validate and sanitize configuration values:

Integer count = findPropertyConfiguration("productCount", configuration, Integer.class)
    .orElse(5);

// Clamp to valid range
count = Math.max(1, Math.min(20, count));

String type = findPropertyConfiguration("alertType", configuration, String.class)
    .orElse("info");

// Validate enum-like values
if (!List.of("info", "success", "warning", "error").contains(type)) {
    type = "info";
}

Templates and Resources

Template Structure

Templates use Thymeleaf's fragment system:

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<th:block th:fragment="extension(model)">
    <!-- Extension content here -->
    <!-- Access model data with ${model.propertyName} -->
</th:block>
</html>

Model Variables

The model map contains:

Key Description Type
templateFilePath Path to template String
pageLocationName Page slot name String
formPrefix Unique ID for forms String
Custom keys Your extension data Any

Accessing Configuration in Templates

<div class="extension">
    <h2 th:text="${model.title}">Default Title</h2>
    <p th:if="${model.showDescription}" th:text="${model.description}">Description</p>

    <ul>
        <li th:each="item : ${model.items}" th:text="${item.name}">Item</li>
    </ul>
</div>

Resource Paths

CSS Files

Add CSS to the model in Java:

requiresCSS(model, "/extensions/myExtension/styles.css");

This maps to:

web-mvc/src/main/resources/resources/settBuilder/themes/{theme}/css/extensions/myExtension/styles.css

JavaScript Files

Add JavaScript to the model in Java:

requiresJS(model, "/extensions/myExtension/script.js");

This maps to:

web-mvc/src/main/resources/resources/settBuilder/themes/{theme}/js/extensions/myExtension/script.js

External Resources

For CDN or external resources:

// External CDN (starts with //)
requiresJS(model, "//cdn.jsdelivr.net/npm/[email protected]/dist/lib.min.js");

// Vendor library (@ prefix)
requiresJS(model, "@vendor/sweetalert2/9.17.2/dist/sweetalert2.all.min.js");

Stripe.js

Stripe.js (https://js.stripe.com/v3/, ~1 MB) is not loaded on every page. The theme head only injects it when an extension rendered on the page calls requiresStripeJS(model), which the ExtensionService lifts to the page model as stripeJsRequired. Any extension whose client code calls Stripe(...) must declare it:

requiresJS(model, "/extensions/myPayment/pay.js");
requiresStripeJS(model);

Stripe.js loads asynchronously, so initialise through the global queue that every page defines:

addStripeInitFunction(function initMyPayment() {
    const stripe = Stripe(publicKey);
    // ...
});

Current consumers: PaymentExtension, UnifiedCheckoutExtension, ApplePayExtension (quick-pay, which can appear on product and basket pages).

A site can opt back into loading Stripe.js on every page with the stripe-js-on-every-page site config key (payment-config, default off). Stripe recommends this so Radar sees the whole browsing session, not just checkout. StripeJsEverywhereInterceptor then sets stripeJsRequired on every rendered page, including pages that don't go through the extension pipeline. Extensions still have to call requiresStripeJS, because the default is off.

Images

Use the shared fragment/responsive-image :: responsiveImage(baseUrl, imagePath, altText) fragment for product/media images. It emits loading="lazy", decoding="async" and width/height by default. For an above-the-fold (LCP) image, set responsiveImagePriority around the include to get loading="eager" fetchpriority="high" instead:

<th:block th:with="responsiveImagePriority=${stat.first}">
    <div th:insert="~{fragment/responsive-image :: responsiveImage(${siteContext.productImagePath}, ${img.url}, ${img.altText})}"></div>
</th:block>

Theme Support

Extensions should support all themes by creating templates for each:

templates/
├── bootstrap/extensions/myExtension/main.html
├── nova/extensions/myExtension/main.html
└── hardware/extensions/myExtension/main.html

Theme resolution: 1. Check for template in active theme (e.g., nova/extensions/myExtension/main.html) 2. Fall back to base theme (e.g., bootstrap/extensions/myExtension/main.html) 3. Error if neither exists

CSS/JS resolution follows the same pattern: - themes/nova/css/extensions/myExtension/styles.css (preferred) - themes/bootstrap/css/extensions/myExtension/styles.css (fallback)

Inline Configuration in Templates

Pass configuration from Java to JavaScript:

<script th:inline="javascript">
    window.myExtensionConfig = {
        enabled: /*[[${model.enabled}]]*/ true,
        count: /*[[${model.count}]]*/ 10,
        apiKey: /*[[${model.apiKey}]]*/ 'default-key'
    };
</script>

Access in JavaScript:

const config = window.myExtensionConfig;
console.log('Extension enabled:', config.enabled);

Testing Extensions

Manual Testing

  1. Build Project

    mvn clean package -DskipTests
    

  2. Start Dev Environment

    ./dev-scripts/devEnv.sh
    

  3. Access Admin Interface

  4. Navigate to https://localhost:9443/admin
  5. Log in with admin credentials

  6. Add Extension

  7. Go to a page, product, or collection
  8. Click "Add Extension"
  9. Select your extension
  10. Configure settings

  11. View Extension

  12. Navigate to the page where you added the extension
  13. Verify rendering and functionality

  14. Test Configuration Changes

  15. Modify extension settings
  16. Refresh page
  17. Verify changes take effect

Unit Testing

Create unit tests for extension logic:

package uk.co.kedos.badger.settbuilder.extensions.content;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;

class AlertBannerExtensionTest {

    private AlertBannerExtension extension;
    private ExtensionUtils extensionUtils;

    @BeforeEach
    void setUp() {
        extensionUtils = mock(ExtensionUtils.class);
        extension = new AlertBannerExtension(extensionUtils);
    }

    @Test
    void testGetName() {
        assertEquals("alertBanner", extension.getName());
    }

    @Test
    void testGetDescription() {
        assertNotNull(extension.getDescription());
        assertTrue(extension.getDescription().length() > 0);
    }

    @Test
    void testEditableFields() {
        var fields = extension.getEditableFields();
        assertNotNull(fields);
        assertEquals(2, fields.size());

        var messageField = fields.get(0);
        assertEquals("message", messageField.getName());
        assertEquals(String.class, messageField.getType());
    }

    @Test
    void testDefaultPageLocation() {
        assertEquals(SlotNames.TOP_BANNER, extension.getDefaultPageLocation());
    }

    @Test
    void testGenerateViewExtensionModel() {
        // Arrange
        var itemBeingRendered = mock(ExtensionConfigurationContainer.class);
        var siteContext = mock(SiteContext.class);
        var configuration = mock(ExtensionConfigurationBean.class);
        var requestContext = mock(ExtensionRequestContext.class);

        // Act
        var model = extension.generateViewExtensionModel(
            itemBeingRendered, siteContext, null, null,
            configuration, requestContext, "ext-123"
        );

        // Assert
        assertNotNull(model);
        assertTrue(model.containsKey("message"));
        assertTrue(model.containsKey("alertType"));
    }
}

Integration Testing

Test extensions in the full application context:

@SpringBootTest
@AutoConfigureMockMvc
class AlertBannerExtensionIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ExtensionService extensionService;

    @Test
    void testExtensionRegistered() {
        var extensions = extensionService.getAllExtensions();
        assertTrue(extensions.stream()
            .anyMatch(ext -> "alertBanner".equals(ext.getName())));
    }

    @Test
    void testExtensionRendering() throws Exception {
        // Create a page with the extension
        // ...

        mockMvc.perform(get("/test-page"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("alert-banner")));
    }
}

Debugging

Enable debug logging:

@Slf4j
public class AlertBannerExtension extends AdministratableBaseBadgerExtensionV2 {

    @Override
    public Map<String, Object> generateViewExtensionModel(...) {
        log.debug("Generating view model for AlertBanner");
        log.debug("Configuration: {}", configuration);

        // ... extension logic

        log.debug("Model created: {}", model);
        return model;
    }
}

View logs:

tail -f logs/application.log | grep AlertBanner


Deployment

Build Process

Extensions are compiled and bundled with the main application:

# Clean build
mvn clean package

# Skip tests for faster builds
mvn clean package -DskipTests

# Include integration tests
./dev-scripts/runTests.sh both

Code Formatting

Extensions must follow Google Java Format with Spotless:

# Format code
mvn spotless:apply

# Check formatting
mvn spotless:check

Note: Don't run Spotless automatically—let developers handle it.

Deployment Checklist

  • [ ] Extension class implements all required methods
  • [ ] Extension name added to messages.properties (admin.extensions.{extensionName}.name=Display Name)
  • [ ] Templates created for all themes (at minimum: bootstrap, nova)
  • [ ] CSS/JS resources added for all themes
  • [ ] Configuration fields properly documented
  • [ ] Unit tests written and passing
  • [ ] Integration tests passing
  • [ ] Code formatted with Spotless
  • [ ] Copyright header present in all files
  • [ ] Extension tested in dev environment
  • [ ] No hardcoded theme names in Java code
  • [ ] Resource paths use relative paths (not absolute)

All files must include the copyright header:

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

Versioning

Use semantic versioning for extensions:

@Override
public String getVersion() {
    return "1.2.3";  // MAJOR.MINOR.PATCH
}
  • MAJOR: Breaking changes
  • MINOR: New features (backwards compatible)
  • PATCH: Bug fixes

Tutorial: Christmas Mode Extension

Let's walk through the complete Christmas Mode extension as a real-world example.

Overview

What it does: - Adds falling snowflakes - Shows snow accumulation on cards - Applies festive color accents - Displays corner emoji decorations - Provides user control via disable button

Features: - Works with all themes - Configurable via admin interface - Performance-optimized animations - Accessibility support (respects prefers-reduced-motion) - localStorage for user preferences

Step 1: Extension Class

commerce-core/src/main/java/.../extensions/seasonal/ChristmasModeExtension.java

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */
package uk.co.kedos.badger.settbuilder.extensions.seasonal;

import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import uk.co.kedos.badger.settbuilder.catalogue.*;
import uk.co.kedos.badger.settbuilder.extensions.*;
import uk.co.kedos.badger.settbuilder.extensions.fields.*;
import uk.co.kedos.badger.settbuilder.extensions.slots.SlotNames;
import uk.co.kedos.badger.settbuilder.extensions.v2.AdministratableBaseBadgerExtensionV2;

@Component
@Slf4j
public class ChristmasModeExtension extends AdministratableBaseBadgerExtensionV2 {

    private static final String EXTENSION_NAME = "christmasMode";
    private static final String DEFAULT_TEMPLATE = "extensions/christmasMode/main";

    public ChristmasModeExtension(ExtensionUtils extensionUtils) {
        super(extensionUtils);
    }

    @Override
    public @NotNull String getDefaultPageLocation() {
        return SlotNames.TOP_BANNER;
    }

    @Override
    public @NotNull List<EditableField<?>> getEditableFields() {
        return List.of(
            new SingleValueEditableField<>(
                "snowflakeCount",
                "Number of Snowflakes",
                50,
                Integer.class,
                "How many snowflakes to show (10-100)"
            ),
            new SingleValueEditableField<>(
                "snowAccumulation",
                "Enable Snow Accumulation",
                true,
                Boolean.class,
                "Show snow accumulating on cards and elements"
            ),
            new SingleValueEditableField<>(
                "festiveColors",
                "Enable Festive Colors",
                true,
                Boolean.class,
                "Apply subtle festive color accents"
            ),
            new SingleValueEditableField<>(
                "showEmojis",
                "Show Festive Emojis",
                true,
                Boolean.class,
                "Add festive emoji decorations"
            )
        );
    }

    @Override
    public List<String> getAdminJSPaths() {
        return null;
    }

    @Override
    public List<String> getAdminCSSPaths() {
        return null;
    }

    @Override
    public Map<String, Object> generateViewExtensionModel(
            ExtensionConfigurationContainer itemBeingRendered,
            SiteContext siteContext,
            User user,
            Order order,
            ExtensionConfigurationBean configuration,
            ExtensionRequestContext requestContext,
            String extensionId) {

        Map<String, Object> model = createModel(
            getDefaultPageLocation(),
            DEFAULT_DISPLAY_PRIORITY,
            DEFAULT_TEMPLATE,
            configuration
        );

        // Extract and validate configuration
        int snowflakeCount = findPropertyConfiguration(
            "snowflakeCount", configuration, Integer.class
        ).orElse(50);

        boolean snowAccumulation = findPropertyConfiguration(
            "snowAccumulation", configuration, Boolean.class
        ).orElse(true);

        boolean festiveColors = findPropertyConfiguration(
            "festiveColors", configuration, Boolean.class
        ).orElse(true);

        boolean showEmojis = findPropertyConfiguration(
            "showEmojis", configuration, Boolean.class
        ).orElse(true);

        // Add to model
        model.put("snowflakeCount", snowflakeCount);
        model.put("snowAccumulation", snowAccumulation);
        model.put("festiveColors", festiveColors);
        model.put("showEmojis", showEmojis);

        // Inject resources
        requiresCSS(model, "/extensions/christmasMode/christmas.css");
        requiresJS(model, "/extensions/christmasMode/christmas.js");

        log.debug("Christmas mode extension loaded with {} snowflakes", snowflakeCount);

        return model;
    }

    @Override
    public Map<String, Object> generateViewExtensionModelForFormPost(
            ExtensionConfigurationContainer itemBeingRendered,
            SiteContext siteContext,
            User user,
            Order order,
            ExtensionConfigurationBean configuration,
            ExtensionRequestContext requestContext,
            String extensionId) {
        return generateViewExtensionModel(
            itemBeingRendered, siteContext, user, order,
            configuration, requestContext, extensionId
        );
    }

    @Override
    public String getName() {
        return EXTENSION_NAME;
    }

    @Override
    public String getDescription() {
        return "Adds festive Christmas effects including snowfall, snow accumulation, "
            + "and subtle color accents. Works with all themes.";
    }

    @Override
    public String getAuthor() {
        return "Kedos Consulting Limited";
    }

    @Override
    public String getVersion() {
        return "1.0.0";
    }

    @Override
    public boolean isGlobal() {
        return false;
    }

    @Override
    public String getSiteId() {
        return ALL_SITES;
    }
}

Key points: - Uses @Component for Spring auto-discovery - Uses @Slf4j for logging - Extends AdministratableBaseBadgerExtensionV2 (V2 pattern) - Exposes 4 configurable fields - Injects CSS and JS resources - Renders in TOP_BANNER slot

Step 2: Add to Resource Bundle

Add the extension name to commerce-core/src/main/resources/messages.properties:

admin.extensions.christmasMode.name=Christmas Mode

Insert it in alphabetical order with other extension entries.

Step 3: Templates

Bootstrap Template: templates/bootstrap/extensions/christmasMode/main.html

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<th:block th:fragment="extension(model)">
    <!-- Pass configuration to JavaScript -->
    <script th:inline="javascript">
        window.christmasModeConfig = {
            snowflakeCount: /*[[${model.snowflakeCount}]]*/ 50,
            snowAccumulation: /*[[${model.snowAccumulation}]]*/ true,
            festiveColors: /*[[${model.festiveColors}]]*/ true,
            showEmojis: /*[[${model.showEmojis}]]*/ true
        };
    </script>

    <!-- Debug metadata -->
    <meta
        name="christmas-mode"
        th:content="'v1.0.0 - ' + ${model.snowflakeCount} + ' snowflakes'"
        th:if="${model.snowflakeCount > 0}"
    />
</th:block>
</html>

Nova Template: (identical)

templates/nova/extensions/christmasMode/main.html

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<th:block th:fragment="extension(model)">
    <script th:inline="javascript">
        window.christmasModeConfig = {
            snowflakeCount: /*[[${model.snowflakeCount}]]*/ 50,
            snowAccumulation: /*[[${model.snowAccumulation}]]*/ true,
            festiveColors: /*[[${model.festiveColors}]]*/ true,
            showEmojis: /*[[${model.showEmojis}]]*/ true
        };
    </script>

    <meta
        name="christmas-mode"
        th:content="'v1.0.0 - ' + ${model.snowflakeCount} + ' snowflakes'"
        th:if="${model.snowflakeCount > 0}"
    />
</th:block>
</html>

Why minimal templates? - All visual effects handled by CSS/JS - Configuration passed via inline script - Theme-agnostic approach - Same template works for all themes

Step 4: CSS (Theme-Agnostic Styling)

Both Bootstrap and Nova use identical CSS (theme-agnostic selectors):

themes/bootstrap/css/extensions/christmasMode/christmas.css themes/nova/css/extensions/christmasMode/christmas.css

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

/* ============================================
   Christmas Mode Extension - Festive Styles
   ============================================ */

/* Snowflake Container */
#christmas-snowflakes {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 9998;
  overflow: hidden;
}

/* Individual Snowflake */
.christmas-snowflake {
  position: absolute;
  top: -10px;
  color: #fff;
  font-size: 1em;
  user-select: none;
  pointer-events: none;
  animation: snowfall linear infinite;
  text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
}

/* Snowfall Animation */
@keyframes snowfall {
  0% {
    transform: translateY(0) rotate(0deg);
    opacity: 1;
  }
  100% {
    transform: translateY(100vh) rotate(360deg);
    opacity: 0.8;
  }
}

/* Snow Accumulation on Cards (theme-agnostic selectors) */
[data-christmas-snow="true"] .card::before,
[data-christmas-snow="true"] .nova-product-card::before,
[data-christmas-snow="true"] .panel::before,
[data-christmas-snow="true"] .well::before,
[data-christmas-snow="true"] .product-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 8px;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.9) 0%,
    rgba(240, 248, 255, 0.7) 50%,
    transparent 100%
  );
  border-radius: 0 0 50% 50% / 0 0 100% 100%;
  pointer-events: none;
  z-index: 1;
  opacity: 0;
  animation: snowAccumulate 2s ease-in forwards;
  animation-delay: 3s;
}

@keyframes snowAccumulate {
  from {
    opacity: 0;
    height: 0;
  }
  to {
    opacity: 1;
    height: 8px;
  }
}

/* Festive Color Accents */
[data-christmas-colors="true"] .btn-primary:hover,
[data-christmas-colors="true"] .nova-btn:hover,
[data-christmas-colors="true"] button[type="submit"]:hover {
  box-shadow: 0 0 20px rgba(220, 38, 38, 0.3),
              0 0 40px rgba(34, 197, 94, 0.2);
}

[data-christmas-colors="true"] .navbar::after,
[data-christmas-colors="true"] .header::after,
[data-christmas-colors="true"] nav::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(
    90deg,
    #dc2626 0%,
    #22c55e 25%,
    #dc2626 50%,
    #22c55e 75%,
    #dc2626 100%
  );
  opacity: 0.3;
  animation: festiveShimmer 3s ease-in-out infinite;
}

@keyframes festiveShimmer {
  0%, 100% { opacity: 0.3; }
  50% { opacity: 0.6; }
}

/* Festive Emoji Decorations */
.christmas-emoji {
  position: fixed;
  font-size: 2rem;
  pointer-events: none;
  z-index: 9997;
  opacity: 0.4;
  user-select: none;
}

.christmas-emoji.top-left {
  top: 20px;
  left: 20px;
  animation: gentle-float 6s ease-in-out infinite;
}

.christmas-emoji.top-right {
  top: 20px;
  right: 20px;
  animation: gentle-float 7s ease-in-out infinite 0.5s;
}

.christmas-emoji.bottom-left {
  bottom: 20px;
  left: 20px;
  animation: gentle-float 8s ease-in-out infinite 1s;
}

.christmas-emoji.bottom-right {
  bottom: 20px;
  right: 20px;
  animation: gentle-float 5s ease-in-out infinite 1.5s;
}

@keyframes gentle-float {
  0%, 100% {
    transform: translateY(0) rotate(0deg);
  }
  50% {
    transform: translateY(-10px) rotate(5deg);
  }
}

/* Disable Button */
#christmas-disable-btn {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 9999;
  padding: 10px 16px;
  background: rgba(220, 38, 38, 0.9);
  color: white;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  transition: all 0.3s ease;
  backdrop-filter: blur(10px);
}

#christmas-disable-btn:hover {
  background: rgba(185, 28, 28, 1);
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}

#christmas-disable-btn::before {
  content: '❄️ ';
  margin-right: 4px;
}

/* Hidden State */
body[data-christmas-disabled="true"] #christmas-snowflakes,
body[data-christmas-disabled="true"] .christmas-emoji {
  display: none !important;
}

/* Accessibility: Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .christmas-snowflake,
  .christmas-emoji {
    animation: none;
    opacity: 0.3;
  }
}

/* Mobile Responsive */
@media (max-width: 768px) {
  .christmas-snowflake {
    font-size: 0.8em;
  }

  .christmas-emoji {
    font-size: 1.5rem;
  }

  #christmas-disable-btn {
    bottom: 10px;
    right: 10px;
    padding: 8px 12px;
    font-size: 12px;
  }
}

Key CSS Features: - Theme-agnostic selectors: .card, .nova-product-card, .panel, etc. - Data attributes: Control features via data-christmas-snow, data-christmas-colors - CSS animations: Hardware-accelerated for performance - Accessibility: Respects prefers-reduced-motion - Responsive: Adjusts for mobile screens - Z-index management: Layers effects properly

Step 5: JavaScript

Both Bootstrap and Nova use identical JavaScript:

themes/bootstrap/js/extensions/christmasMode/christmas.js themes/nova/js/extensions/christmasMode/christmas.js

/* (C)2025 Badger Commerce Limited, a subsidiary of Kedos Consulting Limited */

/**
 * Christmas Mode Extension - JavaScript Controller
 */
(function() {
  'use strict';

  const STORAGE_KEY = 'christmasModeDisabled';
  const SNOWFLAKE_CHARS = ['❄', '❅', '❆'];
  const FESTIVE_EMOJIS = ['🎄', '🎅', '⛄', '🎁', '🔔', '⭐', '🕯️', '🦌'];

  let config = {
    snowflakeCount: 50,
    snowAccumulation: true,
    festiveColors: true,
    showEmojis: true
  };

  /**
   * Initialize Christmas Mode
   */
  function init() {
    // Check if user has disabled Christmas mode
    if (isDisabled()) {
      document.body.setAttribute('data-christmas-disabled', 'true');
      createDisableButton(true);
      return;
    }

    // Read configuration
    readConfig();

    // Apply features
    if (config.snowflakeCount > 0) {
      createSnowfall();
    }

    if (config.snowAccumulation) {
      enableSnowAccumulation();
    }

    if (config.festiveColors) {
      enableFestiveColors();
    }

    if (config.showEmojis) {
      createFestiveEmojis();
    }

    // Create disable button
    createDisableButton(false);

    console.log('🎄 Christmas mode activated!');
  }

  /**
   * Read configuration from window
   */
  function readConfig() {
    if (window.christmasModeConfig) {
      config = { ...config, ...window.christmasModeConfig };
    }

    // Validate snowflake count
    config.snowflakeCount = Math.max(10, Math.min(100, config.snowflakeCount));
  }

  /**
   * Check if disabled via localStorage
   */
  function isDisabled() {
    try {
      return localStorage.getItem(STORAGE_KEY) === 'true';
    } catch (e) {
      return false;
    }
  }

  /**
   * Set disabled state
   */
  function setDisabled(disabled) {
    try {
      if (disabled) {
        localStorage.setItem(STORAGE_KEY, 'true');
      } else {
        localStorage.removeItem(STORAGE_KEY);
      }
    } catch (e) {
      console.warn('Could not save Christmas mode preference:', e);
    }
  }

  /**
   * Create snowfall container and snowflakes
   */
  function createSnowfall() {
    const container = document.createElement('div');
    container.id = 'christmas-snowflakes';
    document.body.appendChild(container);

    for (let i = 0; i < config.snowflakeCount; i++) {
      createSnowflake(container);
    }
  }

  /**
   * Create a single snowflake
   */
  function createSnowflake(container) {
    const snowflake = document.createElement('div');
    snowflake.className = 'christmas-snowflake';
    snowflake.textContent = SNOWFLAKE_CHARS[
      Math.floor(Math.random() * SNOWFLAKE_CHARS.length)
    ];

    // Random properties
    snowflake.style.left = Math.random() * 100 + '%';
    snowflake.style.fontSize = (0.5 + Math.random() * 1) + 'em';
    snowflake.style.opacity = 0.6 + Math.random() * 0.4;

    const duration = 10 + Math.random() * 20;
    const delay = Math.random() * 10;
    snowflake.style.animationDuration = duration + 's';
    snowflake.style.animationDelay = delay + 's';

    container.appendChild(snowflake);

    // Infinite loop: remove and recreate
    setTimeout(() => {
      snowflake.remove();
      if (!isDisabled()) {
        createSnowflake(container);
      }
    }, (duration + delay) * 1000);
  }

  /**
   * Enable snow accumulation
   */
  function enableSnowAccumulation() {
    document.body.setAttribute('data-christmas-snow', 'true');
  }

  /**
   * Enable festive colors
   */
  function enableFestiveColors() {
    document.body.setAttribute('data-christmas-colors', 'true');
  }

  /**
   * Create emoji decorations
   */
  function createFestiveEmojis() {
    document.body.setAttribute('data-christmas-emojis', 'true');

    const positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
    positions.forEach((pos, index) => {
      const emoji = document.createElement('div');
      emoji.className = `christmas-emoji ${pos}`;
      emoji.textContent = FESTIVE_EMOJIS[index % FESTIVE_EMOJIS.length];
      document.body.appendChild(emoji);
    });
  }

  /**
   * Create disable button
   */
  function createDisableButton(isCurrentlyDisabled) {
    const button = document.createElement('button');
    button.id = 'christmas-disable-btn';
    button.textContent = isCurrentlyDisabled
      ? 'Enable Christmas Mode'
      : 'Disable Christmas Mode';
    button.setAttribute('type', 'button');
    button.setAttribute(
      'aria-label',
      isCurrentlyDisabled ? 'Enable festive effects' : 'Disable festive effects'
    );

    button.addEventListener('click', toggleChristmasMode);

    document.body.appendChild(button);
  }

  /**
   * Toggle Christmas mode
   */
  function toggleChristmasMode() {
    const currentlyDisabled = isDisabled();
    setDisabled(!currentlyDisabled);
    window.location.reload();
  }

  /**
   * Cleanup (for SPA navigation)
   */
  function cleanup() {
    const container = document.getElementById('christmas-snowflakes');
    if (container) container.remove();

    document.querySelectorAll('.christmas-emoji').forEach(el => el.remove());

    const button = document.getElementById('christmas-disable-btn');
    if (button) button.remove();

    document.body.removeAttribute('data-christmas-snow');
    document.body.removeAttribute('data-christmas-colors');
    document.body.removeAttribute('data-christmas-emojis');
    document.body.removeAttribute('data-christmas-disabled');
  }

  // Initialize
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }

  // Expose cleanup for SPA frameworks
  window.christmasModeCleanup = cleanup;

})();

JavaScript Features: - IIFE pattern: Avoids global namespace pollution - Configuration from template: Reads window.christmasModeConfig - localStorage persistence: Remembers user preference - Dynamic DOM manipulation: Creates snowflakes and emojis - Performance optimization: Regenerates snowflakes instead of keeping all in DOM - Accessibility: ARIA labels on button - SPA support: Cleanup function exposed

Step 6: Build and Test

# Build
mvn clean package -DskipTests

# Start dev environment
./dev-scripts/devEnv.sh

# Test in browser
# 1. Navigate to https://localhost:9443/admin
# 2. Add Christmas Mode extension to a page
# 3. Configure settings
# 4. View the page
# 5. Test disable button

Step 7: Configuration Testing

Test different configurations:

Subtle Mode: - Snowflakes: 20 - Snow Accumulation: Off - Festive Colors: On - Emojis: Off

Full Festive: - Snowflakes: 100 - Snow Accumulation: On - Festive Colors: On - Emojis: On

Performance Mode: - Snowflakes: 10 - Snow Accumulation: Off - Festive Colors: Off - Emojis: Off

Key Takeaways

  1. Theme-Agnostic Design: Use generic CSS selectors and data attributes
  2. Configuration Flexibility: Expose settings through EditableField
  3. Performance: Optimize animations and DOM manipulation
  4. Accessibility: Support prefers-reduced-motion and ARIA
  5. User Control: Always provide a disable option
  6. Minimal Templates: Let CSS/JS do the heavy lifting
  7. Logging: Use SLF4J for debugging

Best Practices

Code Organization

✅ Do: - Use V2 pattern (AdministratableBaseBadgerExtensionV2) - Group related extensions in packages (e.g., seasonal/, social/, checkout/) - Keep extension logic in Java, presentation in templates - Use constants for strings and magic numbers - Add comprehensive logging with SLF4J

❌ Don't: - Use V1 pattern for new extensions - Hardcode theme names in Java - Mix business logic in templates - Duplicate code across themes

Template Design

✅ Do: - Create templates for all themes (at minimum: bootstrap, nova) - Use data attributes for CSS targeting - Pass configuration via inline scripts - Use semantic HTML - Include accessibility attributes

❌ Don't: - Hardcode values in templates - Use theme-specific class names exclusively - Embed large JavaScript blocks in templates - Forget mobile responsiveness

CSS Best Practices

✅ Do: - Use theme-agnostic selectors - Leverage CSS custom properties when available - Use CSS animations for performance - Support prefers-reduced-motion - Namespace classes (e.g., .christmas-, .alert-)

❌ Don't: - Override core theme styles - Use !important unless absolutely necessary - Hardcode colors (prefer CSS variables) - Forget mobile breakpoints

JavaScript Best Practices

✅ Do: - Use IIFE pattern to avoid global pollution - Read configuration from window object - Handle errors gracefully (try/catch) - Use requestAnimationFrame for animations - Expose cleanup functions for SPA support

❌ Don't: - Pollute global namespace - Assume jQuery is available - Forget error handling - Create memory leaks

Configuration Design

✅ Do: - Provide sensible defaults - Validate and clamp values - Use descriptive field names - Include help text for complex fields - Group related fields

❌ Don't: - Expose every internal variable - Use technical jargon in labels - Forget to validate user input - Make configuration too complex

Performance

✅ Do: - Lazy load resources when possible - Use CSS animations (GPU-accelerated) - Minimize DOM manipulation - Debounce expensive operations - Use will-change for animated elements

❌ Don't: - Create thousands of DOM elements - Use JavaScript for animations that CSS can handle - Animate expensive properties (width, height) - Forget to cleanup on disable

Accessibility

✅ Do: - Support prefers-reduced-motion - Add ARIA labels to interactive elements - Ensure keyboard navigation works - Test with screen readers - Maintain color contrast

❌ Don't: - Rely solely on color for information - Trap keyboard focus - Forget alt text on images - Use animations that could trigger seizures

Testing

✅ Do: - Write unit tests for extension logic - Test all configuration combinations - Test on mobile devices - Test with different themes - Test disable/enable functionality

❌ Don't: - Skip integration tests - Test only on desktop - Assume configuration will be valid - Forget edge cases


Troubleshooting

Extension Not Showing Up

Problem: Extension doesn't appear in admin interface

Solutions: 1. Check if extension has @Component annotation 2. Verify extension class is in correct package 3. Rebuild project: mvn clean package -DskipTests 4. Restart dev environment: ./dev-scripts/devEnv.sh 5. Check logs for Spring bean initialization errors

Template Not Rendering

Problem: Template doesn't render or shows errors

Solutions: 1. Verify template path matches generateViewExtensionModel() return value 2. Check template exists in templates/{theme}/extensions/{name}/ 3. Ensure fragment name is extension(model) 4. Verify Thymeleaf syntax is correct 5. Check for null values in model

CSS Not Loading

Problem: Styles don't apply

Solutions: 1. Verify CSS path in requiresCSS() matches file location 2. Check file exists in themes/{theme}/css/extensions/{name}/ 3. Clear browser cache 4. Check browser console for 404 errors 5. Verify CSS syntax is valid

JavaScript Not Executing

Problem: JavaScript doesn't run

Solutions: 1. Verify JS path in requiresJS() matches file location 2. Check file exists in themes/{theme}/js/extensions/{name}/ 3. Check browser console for errors 4. Verify JavaScript syntax is valid 5. Ensure configuration object is passed correctly

Configuration Not Saving

Problem: Configuration changes don't persist

Solutions: 1. Verify EditableField names match keys used in findPropertyConfiguration() 2. Check extension is properly associated with item/page 3. Verify database connection is working 4. Check logs for save errors 5. Ensure field types match expected types

Performance Issues

Problem: Extension causes lag or slow page loads

Solutions: 1. Reduce number of DOM elements created 2. Use CSS animations instead of JavaScript 3. Debounce expensive operations 4. Optimize animation frame rate 5. Reduce resource file sizes

Theme Compatibility Issues

Problem: Extension works in one theme but not another

Solutions: 1. Create theme-specific templates for all themes 2. Use theme-agnostic CSS selectors 3. Avoid hardcoding theme-specific class names 4. Test with all themes enabled 5. Check template resolution logs


Additional Resources

Example Extensions in Codebase

  • Trending Products: extensions/social/TrendingProducts.java
  • Static Carousel: extensions/html/StaticCarouselExtension.java
  • Apple Pay: extensions/checkout/ApplePayExtension.java
  • Christmas Mode: extensions/seasonal/ChristmasModeExtension.java

Key Files to Reference

  • Base Classes: extensions/BadgerExtension.java, extensions/v2/AdministratableBaseBadgerExtensionV2.java
  • Extension Service: extensions/ExtensionService.java
  • Slot Names: extensions/slots/SlotNames.java
  • Field Types: extensions/fields/*.java

Documentation

  • Feature Overview: /docs/features/extension-system.md
  • Project Setup: CLAUDE.md in repository root
  • Nova Theme Guide: CLAUDE.md Nova Theme section

Getting Help

  • GitHub Issues: Report bugs and request features
  • JavaDoc: Available in codebase for API reference
  • Code Examples: Browse existing extensions in commerce-core/src/main/java/.../extensions/

Summary

Building extensions in Badger Commerce follows a clear pattern:

  1. Extend AdministratableBaseBadgerExtensionV2
  2. Define configuration with EditableField<?>
  3. Create templates for each theme
  4. Add CSS/JS resources
  5. Build and test in dev environment
  6. Deploy with confidence

Extensions enable rapid feature development without core platform changes, provide configuration flexibility for business users, and maintain clean separation of concerns.

Happy extending! 🎄