Theme Customizations
Blocker Outdoors uses Suma Elementor v3.0.0, a custom WordPress theme built on Elementor Pro with 60+ specialized widgets for e-commerce and content presentation.
Theme Architecture
Technology Stack
| Technology | Version | Purpose |
|---|---|---|
| Base Theme | Suma Elementor v3.0.0 | Custom parent theme |
| Elementor Pro | v3.28.3 | Page builder framework |
| Laravel Mix | v6.0.49 | Asset compilation and bundling |
| Preact | v10.11.0 | Lightweight React alternative for widgets |
| Tailwind CSS | v3.4 | Utility-first CSS framework |
| PostCSS | v8.4 | CSS processing and optimization |
| Sass | v1.77 | SCSS preprocessing |
Directory Structure
wp-content/themes/suma-elementor/
├── assets/
│ ├── src/
│ │ ├── js/
│ │ │ ├── components/ # Preact components
│ │ │ │ ├── cart-widget.jsx
│ │ │ │ ├── product-grid.jsx
│ │ │ │ ├── variant-selector.jsx
│ │ │ │ └── search-autocomplete.jsx
│ │ │ ├── modules/
│ │ │ │ ├── analytics.js
│ │ │ │ ├── cart.js
│ │ │ │ └── product.js
│ │ │ └── app.js # Main entry point
│ │ ├── scss/
│ │ │ ├── base/
│ │ │ ├── components/
│ │ │ ├── layouts/
│ │ │ ├── utilities/
│ │ │ └── app.scss # Main entry point
│ │ └── images/
│ ├── dist/ # Compiled assets (gitignored)
│ │ ├── css/
│ │ │ └── app.css
│ │ └── js/
│ │ ├── app.js
│ │ └── app.js.map
├── elementor/
│ ├── widgets/ # Custom Elementor widgets
│ │ ├── product-grid.php
│ │ ├── product-carousel.php
│ │ ├── product-comparison.php
│ │ ├── dealer-locator.php
│ │ ├── hero-slider.php
│ │ ├── testimonial-grid.php
│ │ └── ... (60+ widgets)
│ ├── controls/ # Custom Elementor controls
│ └── dynamic-tags/ # Dynamic content tags
├── inc/
│ ├── theme-setup.php
│ ├── custom-post-types.php
│ ├── template-functions.php
│ └── bigcommerce-overrides.php # BC template customizations
├── template-parts/
│ ├── header/
│ ├── footer/
│ └── content/
├── woocommerce/ # Not used (BigCommerce only)
├── bigcommerce/ # BigCommerce template overrides
│ ├── single-product.php
│ ├── product-card.php
│ ├── cart.php
│ └── components/
│ ├── product-gallery.php
│ ├── product-meta.php
│ └── variant-options.php
├── functions.php
├── style.css
├── package.json
├── webpack.mix.js
└── tailwind.config.js
Build System (Laravel Mix)
Configuration
webpack.mix.js:
const mix = require('laravel-mix');
const path = require('path');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
*/
mix
// JavaScript compilation
.js('assets/src/js/app.js', 'assets/dist/js')
.preact() // Enable Preact compilation
.extract() // Vendor code splitting
// SCSS compilation
.sass('assets/src/scss/app.scss', 'assets/dist/css')
.options({
postCss: [
require('tailwindcss'),
require('autoprefixer'),
],
processCssUrls: false,
})
// Copy static assets
.copyDirectory('assets/src/images', 'assets/dist/images')
// Versioning for cache busting (production only)
.version()
// Source maps (development only)
.sourceMaps(!mix.inProduction())
// Browser sync for live reload (development)
.browserSync({
proxy: 'blockeroutdoors.local',
files: [
'assets/dist/**/*',
'**/*.php',
],
})
// Webpack configuration
.webpackConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'assets/src/js'),
'@components': path.resolve(__dirname, 'assets/src/js/components'),
'@modules': path.resolve(__dirname, 'assets/src/js/modules'),
},
},
externals: {
'jquery': 'jQuery',
},
});
// Production optimizations
if (mix.inProduction()) {
mix
.minify('assets/dist/css/app.css')
.minify('assets/dist/js/app.js')
.options({
terser: {
extractComments: false,
},
});
}
tailwind.config.js:
const colors = require('tailwindcss/colors');
module.exports = {
content: [
'./**/*.php',
'./assets/src/js/**/*.{js,jsx}',
'./elementor/widgets/**/*.php',
],
theme: {
extend: {
colors: {
// Blocker Outdoors brand colors
primary: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#2c5f2d', // Primary brand green
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
accent: {
DEFAULT: '#97c93d', // Lime green accent
light: '#b4dc58',
dark: '#7aa82f',
},
camo: {
DEFAULT: '#4a5134',
light: '#6b7352',
dark: '#2d3120',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
heading: ['Oswald', 'system-ui', 'sans-serif'],
display: ['Bebas Neue', 'system-ui', 'sans-serif'],
},
fontSize: {
'display': ['4.5rem', { lineHeight: '1', letterSpacing: '-0.02em' }],
},
spacing: {
'18': '4.5rem',
'88': '22rem',
'128': '32rem',
},
maxWidth: {
'8xl': '88rem',
'9xl': '96rem',
},
zIndex: {
'60': '60',
'70': '70',
'80': '80',
'90': '90',
'100': '100',
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/container-queries'),
],
};
Build Commands
# Development build (watch mode)
npm run dev
# Production build (minified)
npm run production
# Watch for changes and rebuild
npm run watch
# Build with hot module replacement
npm run hot
Asset Enqueueing
functions.php:
<?php
/**
* Enqueue scripts and styles
*/
function suma_enqueue_assets() {
$manifest = include get_template_directory() . '/assets/dist/mix-manifest.php';
// Main stylesheet
wp_enqueue_style(
'suma-app',
get_template_directory_uri() . '/assets/dist/css/app.css',
[],
$manifest['/css/app.css'] ?? '1.0.0'
);
// Vendor JavaScript (extracted dependencies)
wp_enqueue_script(
'suma-vendor',
get_template_directory_uri() . '/assets/dist/js/vendor.js',
[],
$manifest['/js/vendor.js'] ?? '1.0.0',
true
);
// Main JavaScript bundle
wp_enqueue_script(
'suma-app',
get_template_directory_uri() . '/assets/dist/js/app.js',
[ 'suma-vendor', 'jquery' ],
$manifest['/js/app.js'] ?? '1.0.0',
true
);
// Localize script with PHP data
wp_localize_script( 'suma-app', 'sumaConfig', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'restUrl' => rest_url(),
'nonce' => wp_create_nonce( 'wp_rest' ),
'bigcommerce' => [
'storeUrl' => defined( 'BIGCOMMERCE_STORE_URL' ) ? BIGCOMMERCE_STORE_URL : '',
'channelId' => defined( 'BIGCOMMERCE_CHANNEL_ID' ) ? BIGCOMMERCE_CHANNEL_ID : '',
],
'klaviyo' => [
'publicKey' => get_option( 'klaviyo_public_key' ),
],
'algolia' => [
'appId' => defined( 'ALGOLIA_APP_ID' ) ? ALGOLIA_APP_ID : '',
'searchKey' => defined( 'ALGOLIA_SEARCH_KEY' ) ? ALGOLIA_SEARCH_KEY : '',
'indexName' => 'blocker_products',
],
] );
}
add_action( 'wp_enqueue_scripts', 'suma_enqueue_assets' );
Custom Elementor Widgets
Suma Elementor v3.0.0 includes 60+ custom widgets. Here are the key e-commerce widgets:
Product Grid Widget
elementor/widgets/product-grid.php:
<?php
namespace Suma_Elementor\Widgets;
use Elementor\Widget_Base;
use Elementor\Controls_Manager;
class Product_Grid extends Widget_Base {
public function get_name() {
return 'suma_product_grid';
}
public function get_title() {
return __( 'Product Grid', 'suma-elementor' );
}
public function get_icon() {
return 'eicon-products';
}
public function get_categories() {
return [ 'suma-ecommerce' ];
}
protected function register_controls() {
// Content Section
$this->start_controls_section(
'content_section',
[
'label' => __( 'Content', 'suma-elementor' ),
'tab' => Controls_Manager::TAB_CONTENT,
]
);
$this->add_control(
'product_source',
[
'label' => __( 'Product Source', 'suma-elementor' ),
'type' => Controls_Manager::SELECT,
'default' => 'category',
'options' => [
'category' => __( 'Category', 'suma-elementor' ),
'featured' => __( 'Featured Products', 'suma-elementor' ),
'sale' => __( 'On Sale', 'suma-elementor' ),
'new' => __( 'New Arrivals', 'suma-elementor' ),
'manual' => __( 'Manual Selection', 'suma-elementor' ),
],
]
);
$this->add_control(
'category_ids',
[
'label' => __( 'Categories', 'suma-elementor' ),
'type' => Controls_Manager::SELECT2,
'multiple' => true,
'options' => $this->get_product_categories(),
'condition' => [
'product_source' => 'category',
],
]
);
$this->add_control(
'products_per_page',
[
'label' => __( 'Products Per Page', 'suma-elementor' ),
'type' => Controls_Manager::NUMBER,
'default' => 12,
'min' => 1,
'max' => 100,
]
);
$this->add_control(
'columns',
[
'label' => __( 'Columns', 'suma-elementor' ),
'type' => Controls_Manager::SELECT,
'default' => '4',
'options' => [
'2' => '2',
'3' => '3',
'4' => '4',
'5' => '5',
'6' => '6',
],
]
);
$this->add_control(
'show_filters',
[
'label' => __( 'Show Filters', 'suma-elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
]
);
$this->add_control(
'show_sorting',
[
'label' => __( 'Show Sorting', 'suma-elementor' ),
'type' => Controls_Manager::SWITCHER,
'default' => 'yes',
]
);
$this->end_controls_section();
// Style Section
$this->start_controls_section(
'style_section',
[
'label' => __( 'Style', 'suma-elementor' ),
'tab' => Controls_Manager::TAB_STYLE,
]
);
$this->add_control(
'card_style',
[
'label' => __( 'Card Style', 'suma-elementor' ),
'type' => Controls_Manager::SELECT,
'default' => 'standard',
'options' => [
'standard' => __( 'Standard', 'suma-elementor' ),
'minimal' => __( 'Minimal', 'suma-elementor' ),
'overlay' => __( 'Overlay', 'suma-elementor' ),
],
]
);
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
$query_args = [
'post_type' => 'bigcommerce_product',
'posts_per_page' => $settings['products_per_page'],
'post_status' => 'publish',
];
// Build query based on source
if ( $settings['product_source'] === 'category' && ! empty( $settings['category_ids'] ) ) {
$query_args['tax_query'] = [
[
'taxonomy' => 'bigcommerce_category',
'field' => 'term_id',
'terms' => $settings['category_ids'],
],
];
} elseif ( $settings['product_source'] === 'featured' ) {
$query_args['meta_query'] = [
[
'key' => 'bigcommerce_featured',
'value' => '1',
],
];
} elseif ( $settings['product_source'] === 'sale' ) {
$query_args['meta_query'] = [
[
'key' => 'bigcommerce_sale_price',
'value' => 0,
'compare' => '>',
'type' => 'NUMERIC',
],
];
}
$products = new \WP_Query( $query_args );
if ( $products->have_posts() ) :
?>
<div class="suma-product-grid" data-columns="<?php echo esc_attr( $settings['columns'] ); ?>">
<?php if ( $settings['show_filters'] === 'yes' ) : ?>
<div class="product-filters">
<?php echo do_shortcode( '[facetwp template="filters"]' ); ?>
</div>
<?php endif; ?>
<?php if ( $settings['show_sorting'] === 'yes' ) : ?>
<div class="product-sorting">
<?php echo do_shortcode( '[facetwp sort="true"]' ); ?>
</div>
<?php endif; ?>
<div class="product-grid grid grid-cols-<?php echo esc_attr( $settings['columns'] ); ?> gap-6">
<?php
while ( $products->have_posts() ) :
$products->the_post();
$this->render_product_card( $settings['card_style'] );
endwhile;
wp_reset_postdata();
?>
</div>
<?php if ( $products->max_num_pages > 1 ) : ?>
<div class="product-pagination">
<?php echo do_shortcode( '[facetwp pager="true"]' ); ?>
</div>
<?php endif; ?>
</div>
<?php
endif;
}
private function render_product_card( $style = 'standard' ) {
$product_id = get_the_ID();
$bc_id = get_post_meta( $product_id, 'bigcommerce_id', true );
$price = get_post_meta( $product_id, 'bigcommerce_price', true );
$sale_price = get_post_meta( $product_id, 'bigcommerce_sale_price', true );
$availability = get_post_meta( $product_id, 'bigcommerce_availability', true );
?>
<article class="product-card product-card--<?php echo esc_attr( $style ); ?>" data-product-id="<?php echo esc_attr( $bc_id ); ?>">
<div class="product-card__image">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'medium', [ 'class' => 'w-full h-auto' ] ); ?>
</a>
<?php if ( $availability !== 'available' ) : ?>
<span class="product-badge product-badge--out-of-stock">
<?php _e( 'Out of Stock', 'suma-elementor' ); ?>
</span>
<?php elseif ( $sale_price > 0 && $sale_price < $price ) : ?>
<span class="product-badge product-badge--sale">
<?php _e( 'Sale', 'suma-elementor' ); ?>
</span>
<?php endif; ?>
<div class="product-card__quick-actions">
<button class="btn-quick-view" data-product-id="<?php echo esc_attr( $bc_id ); ?>">
<?php _e( 'Quick View', 'suma-elementor' ); ?>
</button>
</div>
</div>
<div class="product-card__content">
<h3 class="product-card__title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h3>
<div class="product-card__price">
<?php if ( $sale_price > 0 && $sale_price < $price ) : ?>
<span class="price price--regular"><?php echo esc_html( '$' . number_format( $price, 2 ) ); ?></span>
<span class="price price--sale"><?php echo esc_html( '$' . number_format( $sale_price, 2 ) ); ?></span>
<?php else : ?>
<span class="price"><?php echo esc_html( '$' . number_format( $price, 2 ) ); ?></span>
<?php endif; ?>
</div>
<div class="product-card__actions">
<button class="btn btn-primary btn-add-to-cart" data-product-id="<?php echo esc_attr( $bc_id ); ?>">
<?php _e( 'Add to Cart', 'suma-elementor' ); ?>
</button>
</div>
</div>
</article>
<?php
}
private function get_product_categories() {
$categories = get_terms( [
'taxonomy' => 'bigcommerce_category',
'hide_empty' => false,
] );
$options = [];
foreach ( $categories as $category ) {
$options[ $category->term_id ] = $category->name;
}
return $options;
}
}
Product Carousel Widget
Advanced carousel with Swiper.js integration for featured products, related items, and promotional displays.
Variant Selector Widget (Preact)
assets/src/js/components/variant-selector.jsx:
import { h, Component } from 'preact';
import { useState, useEffect } from 'preact/hooks';
export default function VariantSelector({ productId, variants, onVariantChange }) {
const [selectedOptions, setSelectedOptions] = useState({});
const [selectedVariant, setSelectedVariant] = useState(null);
const [price, setPrice] = useState(null);
const [availability, setAvailability] = useState('available');
// Get unique option types
const optionTypes = [...new Set(variants.flatMap(v =>
v.option_values.map(ov => ov.option_display_name)
))];
// Get option values for each option type
const getOptionValues = (optionType) => {
const values = new Set();
variants.forEach(variant => {
const option = variant.option_values.find(ov =>
ov.option_display_name === optionType
);
if (option) {
values.add(option.label);
}
});
return Array.from(values);
};
// Handle option selection
const handleOptionChange = (optionType, value) => {
const newOptions = {
...selectedOptions,
[optionType]: value
};
setSelectedOptions(newOptions);
// Find matching variant
const variant = variants.find(v => {
return Object.keys(newOptions).every(optType => {
const option = v.option_values.find(ov =>
ov.option_display_name === optType
);
return option && option.label === newOptions[optType];
});
});
if (variant) {
setSelectedVariant(variant);
setPrice(variant.sale_price || variant.price);
setAvailability(variant.inventory_level > 0 ? 'available' : 'out_of_stock');
if (onVariantChange) {
onVariantChange(variant);
}
}
};
return (
<div className="variant-selector">
{optionTypes.map(optionType => (
<div key={optionType} className="variant-selector__option">
<label className="variant-selector__label">
{optionType}:
{selectedOptions[optionType] && (
<span className="variant-selector__selected">
{' '}{selectedOptions[optionType]}
</span>
)}
</label>
<div className="variant-selector__values">
{getOptionValues(optionType).map(value => (
<button
key={value}
type="button"
className={`variant-selector__button ${
selectedOptions[optionType] === value ? 'active' : ''
}`}
onClick={() => handleOptionChange(optionType, value)}
>
{value}
</button>
))}
</div>
</div>
))}
{price && (
<div className="variant-selector__price">
<span className="price">${price.toFixed(2)}</span>
</div>
)}
{availability === 'out_of_stock' && (
<div className="variant-selector__availability">
<span className="out-of-stock">Out of Stock</span>
</div>
)}
</div>
);
}
BigCommerce Template Overrides
Custom templates in bigcommerce/ directory override plugin defaults:
Single Product Template
bigcommerce/single-product.php:
<?php
/**
* Single Product Template Override
* Customized for Blocker Outdoors
*/
get_header();
while ( have_posts() ) :
the_post();
$product_id = get_the_ID();
$bc_id = get_post_meta( $product_id, 'bigcommerce_id', true );
$price = get_post_meta( $product_id, 'bigcommerce_price', true );
$sale_price = get_post_meta( $product_id, 'bigcommerce_sale_price', true );
$sku = get_post_meta( $product_id, 'bigcommerce_sku', true );
$brand = get_post_meta( $product_id, 'bigcommerce_brand', true );
?>
<article id="product-<?php echo esc_attr( $bc_id ); ?>" <?php post_class( 'single-product' ); ?>>
<div class="product-layout container mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Product Images -->
<div class="product-images">
<?php get_template_part( 'bigcommerce/components/product-gallery' ); ?>
</div>
<!-- Product Info -->
<div class="product-info">
<!-- Breadcrumbs -->
<div class="product-breadcrumbs mb-4">
<?php echo do_shortcode( '[rank_math_breadcrumb]' ); ?>
</div>
<!-- Brand -->
<?php if ( $brand ) : ?>
<div class="product-brand mb-2">
<span class="text-sm text-gray-600"><?php echo esc_html( $brand ); ?></span>
</div>
<?php endif; ?>
<!-- Title -->
<h1 class="product-title text-3xl font-heading font-bold mb-4">
<?php the_title(); ?>
</h1>
<!-- Price -->
<div class="product-price mb-6">
<?php if ( $sale_price > 0 && $sale_price < $price ) : ?>
<span class="price price--regular line-through text-gray-500 mr-2">
$<?php echo number_format( $price, 2 ); ?>
</span>
<span class="price price--sale text-3xl font-bold text-accent">
$<?php echo number_format( $sale_price, 2 ); ?>
</span>
<?php else : ?>
<span class="price text-3xl font-bold">
$<?php echo number_format( $price, 2 ); ?>
</span>
<?php endif; ?>
</div>
<!-- Reviews -->
<div class="product-reviews mb-6">
<?php echo do_shortcode( '[bazaarvoice_reviews product_id="' . $bc_id . '"]' ); ?>
</div>
<!-- Description -->
<div class="product-description mb-6">
<?php the_content(); ?>
</div>
<!-- Variant Selector -->
<div class="product-variants mb-6" data-product-id="<?php echo esc_attr( $bc_id ); ?>">
<?php get_template_part( 'bigcommerce/components/variant-options' ); ?>
</div>
<!-- Add to Cart -->
<div class="product-actions mb-6">
<button class="btn btn-primary btn-lg w-full mb-4" id="add-to-cart">
<?php _e( 'Add to Cart', 'suma-elementor' ); ?>
</button>
<!-- Financing Options -->
<div class="product-financing grid grid-cols-2 gap-4">
<div class="klarna-messaging" data-purchase-amount="<?php echo esc_attr( $sale_price ?: $price ); ?>">
<!-- Klarna widget injected via JavaScript -->
</div>
<div class="sezzle-widget" data-amount="<?php echo esc_attr( ( $sale_price ?: $price ) * 100 ); ?>">
<!-- Sezzle widget injected via JavaScript -->
</div>
</div>
</div>
<!-- Product Meta -->
<div class="product-meta text-sm text-gray-600 space-y-2">
<?php if ( $sku ) : ?>
<div>
<strong><?php _e( 'SKU:', 'suma-elementor' ); ?></strong>
<span><?php echo esc_html( $sku ); ?></span>
</div>
<?php endif; ?>
<div>
<strong><?php _e( 'Categories:', 'suma-elementor' ); ?></strong>
<?php echo get_the_term_list( $product_id, 'bigcommerce_category', '', ', ' ); ?>
</div>
</div>
<!-- Back in Stock -->
<div class="product-back-in-stock mt-6">
<?php echo do_shortcode( '[back_in_stock_form product_id="' . $bc_id . '"]' ); ?>
</div>
</div>
</div>
<!-- Product Tabs -->
<div class="product-tabs mt-12">
<?php get_template_part( 'bigcommerce/components/product-tabs' ); ?>
</div>
<!-- Related Products -->
<div class="related-products mt-12">
<h2 class="text-2xl font-heading font-bold mb-6"><?php _e( 'Related Products', 'suma-elementor' ); ?></h2>
<?php echo do_shortcode( '[suma_product_carousel source="related" product_id="' . $bc_id . '"]' ); ?>
</div>
</div>
</article>
<?php
endwhile;
get_footer();
Responsive Design
Mobile-first approach with Tailwind CSS breakpoints:
/* Tailwind breakpoints */
sm: 640px
md: 768px
lg: 1024px
xl: 1280px
2xl: 1536px
/* Custom breakpoints for product grids */
@screen md {
.product-grid[data-columns="4"],
.product-grid[data-columns="5"],
.product-grid[data-columns="6"] {
@apply grid-cols-2;
}
}
@screen lg {
.product-grid[data-columns="4"] {
@apply grid-cols-3;
}
.product-grid[data-columns="5"],
.product-grid[data-columns="6"] {
@apply grid-cols-4;
}
}
@screen xl {
.product-grid[data-columns="4"] {
@apply grid-cols-4;
}
.product-grid[data-columns="5"] {
@apply grid-cols-5;
}
.product-grid[data-columns="6"] {
@apply grid-cols-6;
}
}
Performance Optimizations
Lazy Loading
// Lazy load product images
import 'lazysizes';
document.addEventListener('DOMContentLoaded', () => {
// Add lazyload class to images
document.querySelectorAll('.product-card__image img').forEach(img => {
img.classList.add('lazyload');
img.dataset.src = img.src;
img.src = '/wp-content/themes/suma-elementor/assets/dist/images/placeholder.svg';
});
});
Code Splitting
// Dynamic imports for non-critical components
const loadQuickView = () => import(/* webpackChunkName: "quick-view" */ '@components/quick-view');
const loadProductComparison = () => import(/* webpackChunkName: "comparison" */ '@components/product-comparison');
// Load on demand
document.querySelectorAll('.btn-quick-view').forEach(btn => {
btn.addEventListener('click', async () => {
const { default: QuickView } = await loadQuickView();
const productId = btn.dataset.productId;
QuickView.open(productId);
});
});