Process Oct 30, 2025 8 min read

Procurato's Website: an Enterprise WordPress Development

A comprehensive WordPress solution for a UK procurement company featuring dynamic animations, intuitive service navigation, and a scalable modular architecture.

Lushano Perera
Lushano Perera
Author

A modern, modular WordPress theme architecture for a UK procurement company, featuring GSAP animations, Docker-based development, and enterprise-grade code organization.


Project Overview

The Challenge

Procurato needed a corporate website that could showcase their procurement, insurance, and data analytics services while maintaining a sophisticated, professional appearance. The existing setup required modernization to support:

  • Complex page hierarchies with intuitive navigation
  • Dynamic content features with smooth animations
  • Scalable architecture for future feature development
  • Efficient deployment workflow for rapid iterations
  • Enterprise-grade code quality and maintainability

The Solution

I developed a custom Hello Elementor child theme using modern PHP architecture patterns, implementing a feature-based modular system that separates concerns, enables conditional loading, and provides a foundation for scalable WordPress development.


Technology Stack

Backend

TechnologyVersionPurpose
WordPress6.xContent Management System
PHP8.3+Server-side language with strict typing
MariaDB10.11Database engine
RedisLatestObject caching (optional)

Frontend

TechnologyVersionPurpose
Elementor ProLatestPage builder integration
GSAP3.12.2Animation library
ScrollTrigger3.12.2Scroll-based animations
SCSS/Sass1.69+CSS preprocessing

Build Tools

TechnologyVersionPurpose
Webpack5.xModule bundler
Babel7.xJavaScript transpilation
PostCSS8.xCSS transformations
ESLint8.xJavaScript linting
Stylelint15.xCSS/SCSS linting

DevOps & Testing

TechnologyPurpose
DockerLocal development environment
NGINXWeb server
rsyncDeployment automation
PlaywrightEnd-to-end testing
PHPUnitUnit testing
PHPCSPHP code standards

Architecture Deep-Dive

Modular Theme Architecture

The theme follows a feature-based modular architecture that separates concerns and enables clean, maintainable code:

hello-theme-procurato/
├── inc/ # PHP business logic
│ ├── abstracts/ # Base classes & traits
│ │ ├── class-feature.php # Feature base class
│ │ └── trait-singleton.php # Singleton pattern
│ ├── core/ # Always-loaded components
│ │ ├── class-asset-manager.php
│ │ └── class-theme-support.php
│ ├── features/ # Self-contained modules
│ │ ├── class-text-rotator.php
│ │ ├── class-animated-gallery.php
│ │ ├── class-awards-carousel.php
│ │ ├── class-shortcodes.php
│ │ └── class-acf-dynamic-population.php
│ └── class-theme.php # Main orchestrator
├── assets/
│ ├── css/src/ # SCSS source files
│ ├── css/dist/ # Compiled CSS
│ ├── js/src/ # JavaScript modules
│ └── js/dist/ # Compiled JavaScript
└── functions.php # Bootstrap & autoloader

PSR-4 Autoloading

Custom autoloader implementation that maps namespaces to file paths:

<?php
declare(strict_types=1);

// Theme constants
define('HELLO_ELEMENTOR_CHILD_VERSION', '2.0.0');
define('HELLO_ELEMENTOR_CHILD_PATH', get_stylesheet_directory());
define('HELLO_ELEMENTOR_CHILD_INC', HELLO_ELEMENTOR_CHILD_PATH . '/inc');

// PSR-4 style autoloader
spl_autoload_register(function ($class) {
$namespace = 'HelloElementorChild\\';
if (strpos($class, $namespace) !== 0) {
return;
}

$class = str_replace($namespace, '', $class);
$class_parts = explode('\\', $class);

if (count($class_parts) > 1) {
$directory = strtolower($class_parts[0]);
$class_name = $class_parts[1];
$class_file = strtolower(str_replace('_', '-', $class_name));
$file = HELLO_ELEMENTOR_CHILD_INC . '/' . $directory . '/class-' . $class_file . '.php';

if (file_exists($file)) {
require_once $file;
}
}
});

// Initialize theme
HelloElementorChild\Theme::get_instance();

Singleton Pattern

All components inherit from a Singleton trait ensuring single instance throughout execution:

<?php
namespace HelloElementorChild\Abstracts;

trait Singleton {
private static ?self $instance = null;

public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}

private function __clone() {}
public function __wakeup() {
throw new \Exception("Cannot unserialize singleton");
}
}

Key Features Built

1. Text Rotator – GSAP-Powered Animated Text

Challenge: Create engaging hero sections with rotating text that maintains accessibility and performs well.

Solution: A shortcode-based system with 6 animation effects, ARIA live regions for screen readers, and respect for user motion preferences.

<?php
// Usage: [text_rotator texts="Innovation|Excellence|Quality" effect="fade" speed="3000"]

final class Text_Rotator extends Feature {
private array $available_effects = [
'fade', 'slide', 'flip', 'type', 'bounce', 'rotate'
];

private array $default_config = [
'effect' => 'fade',
'speed' => 3000,
'pause_hover' => true,
'auto_start' => true,
'loop' => true,
];

public function render_shortcode($atts, $content = null): string {
$atts = shortcode_atts($this->default_config, $atts);

// Conditional asset loading
$this->enqueue_assets();

return sprintf(
'<span class="text-rotator" data-effect="%s" data-speed="%d"
aria-live="polite" aria-atomic="true">%s</span>',
esc_attr($atts['effect']),
intval($atts['speed']),
esc_html($texts[0])
);
}
}

Features:

  • 6 animation effects (fade, slide, flip, type, bounce, rotate)
  • Configurable speed, hover pause, auto-start
  • ARIA live regions for accessibility
  • Respects prefers-reduced-motion
  • Elementor widget placeholder ready

Challenge: Create smooth horizontal scroll galleries that perform well and work with Elementor’s gallery widgets.

Solution: GSAP ScrollTrigger integration with intelligent detection of gallery elements and conditional script loading.

<?php
final class Animated_Gallery extends Feature {
private array $gsap_versions = [
'core' => '3.12.2',
'scrolltrigger' => '3.12.2',
];

private array $gallery_selectors = [
'.animated-gallery',
'.horizontal-scroll-gallery',
'.gsap-carousel',
];

private array $gsap_config = [
'use_cdn' => true,
'conditional_load' => true,
'defer_loading' => true,
];

public function has_animated_galleries(): bool {
global $post;

// Check Elementor data for gallery widgets
$elementor_data = get_post_meta($post->ID, '_elementor_data', true);
if ($elementor_data) {
$data = json_decode($elementor_data, true);
return $this->search_for_gallery_widgets($data);
}

return false;
}
}

Features:

  • CDN-loaded GSAP for optimal performance
  • Multiple gallery selector support
  • Elementor widget detection
  • ACF gallery field detection
  • Responsive breakpoints (mobile, tablet, desktop)

3. Page Accordion – Dynamic Navigation

Challenge: Display complex page hierarchies in a compact, accessible accordion format that matches Elementor’s design system.

Solution: A shortcode that generates Elementor-styled accordions with full keyboard navigation and proper ARIA attributes.

<?php
// Usage: [page_accordion parent_ids="648,735,760"]

public function page_accordion($atts): string {
$atts = shortcode_atts([
'parent_ids' => '648,735,760', // Default: Procurement, Insurance, Data
'layout' => 'accordion',
], $atts);

$parent_ids = array_map('intval', explode(',', $atts['parent_ids']));

ob_start();
?>
<div class="procurato-page-accordion" role="tablist">
<?php foreach ($parent_ids as $parent_id):
$parent = get_post($parent_id);
$children = get_pages(['parent' => $parent_id, 'sort_column' => 'menu_order']);
?>
<div class="accordion-item">
<h3 class="accordion-header" role="tab" aria-expanded="false">
<a href="<?php echo get_permalink($parent_id); ?>">
<?php echo esc_html($parent->post_title); ?>
</a>
<button class="accordion-toggle" aria-label="Toggle submenu">
<svg><!-- Chevron icon --></svg>
</button>
</h3>
<div class="accordion-content" role="tabpanel" hidden>
<?php $this->render_page_hierarchy($children); ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}

Features:

  • Dynamic page hierarchy display (3+ levels)
  • Elementor CSS variables for theming
  • Full keyboard navigation (arrows, Enter, Escape)
  • ARIA labels and states
  • 12-hour transient caching
  • Print-friendly styles

4. ACF Dynamic Population

Challenge: Automatically populate ACF select fields with taxonomy terms while maintaining hierarchy visualization.

Solution: A flexible field population system that supports taxonomies, posts, and users with hierarchical display.

<?php
final class ACF_Dynamic_Population extends Feature {
private array $field_configs = [
'related_topic' => [
'taxonomy' => 'post_topic',
'empty_text' => 'Please select a topic...',
'orderby' => 'name',
'show_hierarchy' => true,
'hierarchy_separator' => ' > ',
],
];

public function populate_taxonomy_field(array $field, array $config): array {
$terms = get_terms([
'taxonomy' => $config['taxonomy'],
'hide_empty' => $config['hide_empty'] ?? false,
'orderby' => $config['orderby'],
]);

$field['choices'] = ['' => $config['empty_text']];

foreach ($terms as $term) {
$label = $config['show_hierarchy']
? $this->get_term_hierarchy($term, $config['hierarchy_separator'])
: $term->name;
$field['choices'][$term->term_id] = $label;
}

return $field;
}
}

5. FacetWP Custom Loader

Challenge: Provide visual feedback during FacetWP filtering with branded loading animation.

Solution: Custom Lottie animation integration with overlay support.

<?php
private array $loader_config = [
'enabled' => true,
'type' => 'lottie_animation',
'lottie_url' => '/wp-content/uploads/loading-procurato.json',
'selector' => '.facetwp-template',
'overlay' => true,
'animation_duration' => 300,
'lottie_speed' => 1,
'lottie_loop' => true,
];

DevOps & Workflow

Docker Development Environment

Local development runs in isolated Docker containers:

# docker-compose.yml structure
services:
nginx: # Web server on port 80
php: # PHP 8.3 with WordPress
mariadb: # Database on port 3307
redis: # Object cache on port 6380
phpmyadmin: # Database UI on port 8082
mailhog: # Email testing on port 8026

Benefits:

  • Consistent environment across machines
  • Easy onboarding for team members
  • Production-like local setup
  • Isolated service debugging

Webpack Build Pipeline

Modern asset compilation with optimization:

// webpack.config.js
module.exports = (env, argv) => ({
entry: {
main: './assets/js/src/main.js',
'text-rotator': './assets/js/src/modules/text-rotator.js',
'animated-gallery': './assets/js/src/modules/animated-gallery.js',
style: './assets/css/src/main.scss'
},
output: {
path: path.resolve(__dirname, 'assets'),
filename: 'js/dist/[name].js',
},
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' },
{ test: /\.(sa|sc|c)ss$/, use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]}
]
},
optimization: {
minimize: isProduction,
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()]
}
});

Build Commands:

npm run build    # Production build
npm run dev # Development with watch
npm run lint # Full linting suite

Deployment Strategy

Efficient rsync-based deployment for fast iterations:

# Deploy theme to staging (excludes dev files)
rsync -avz --progress \
--exclude='node_modules' \
--exclude='vendor' \
--exclude='*.map' \
--exclude='assets/*/src' \
./wp-content/themes/hello-theme-procurato/ \
server:/path/to/themes/hello-theme-procurato/

Challenges & Solutions

1. Performance: Conditional Script Loading

Problem: GSAP libraries are large; loading them on every page impacts performance.

Solution: Intelligent content detection that only loads scripts when needed:

public function should_enqueue_scripts(): bool {
// Check for gallery CSS classes in content
if ($this->has_animated_galleries()) {
return true;
}

// Check Elementor data for specific widgets
$elementor_data = get_post_meta(get_the_ID(), '_elementor_data', true);
if ($elementor_data) {
return $this->detect_gallery_widgets(json_decode($elementor_data, true));
}

return false;
}

2. Accessibility Compliance

Problem: Animation-heavy features can be problematic for users with motion sensitivity.

Solution: Respect prefers-reduced-motion at both CSS and JavaScript levels:

@media (prefers-reduced-motion: reduce) {
.text-rotator,
.animated-gallery {
animation: none !important;
transition: none !important;
}
}

3. Elementor Integration

Problem: Detecting Elementor widgets from stored JSON data for conditional loading.

Solution: Recursive JSON parsing to find specific widget types:

private function search_for_gallery_widgets(array $elements): bool {
$gallery_widgets = ['image-carousel', 'media-carousel', 'image-gallery'];

foreach ($elements as $element) {
if (isset($element['widgetType']) && in_array($element['widgetType'], $gallery_widgets)) {
return true;
}
if (!empty($element['elements'])) {
if ($this->search_for_gallery_widgets($element['elements'])) {
return true;
}
}
}
return false;
}

Results & Impact

Performance Improvements

  • Conditional loading reduced average page script size by ~40%
  • CSS-only scroll snap eliminated JavaScript for purpose flow section
  • Optimized animations using GPU acceleration (transform, opacity)

Code Quality

  • PSR-4 autoloading for clean class organization
  • Strict typing (PHP 8.3 declare(strict_types=1))
  • PHPCS compliance with WordPress coding standards
  • Feature isolation enabling easy maintenance and testing

Developer Experience

  • Modular architecture allows adding features without touching core files
  • Comprehensive documentation in CLAUDE.md files throughout the theme
  • Automated deployment via rsync scripts
  • Local Docker environment for consistent development

Accessibility

  • ARIA live regions for dynamic content announcements
  • Keyboard navigation for all interactive components
  • Motion preferences respected throughout
  • Semantic HTML structure

Written by Lushano Perera

Digital craftsman exploring the intersection of design, technology, and human experience.