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.
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
| Technology | Version | Purpose |
|---|---|---|
| WordPress | 6.x | Content Management System |
| PHP | 8.3+ | Server-side language with strict typing |
| MariaDB | 10.11 | Database engine |
| Redis | Latest | Object caching (optional) |
Frontend
| Technology | Version | Purpose |
|---|---|---|
| Elementor Pro | Latest | Page builder integration |
| GSAP | 3.12.2 | Animation library |
| ScrollTrigger | 3.12.2 | Scroll-based animations |
| SCSS/Sass | 1.69+ | CSS preprocessing |
Build Tools
| Technology | Version | Purpose |
|---|---|---|
| Webpack | 5.x | Module bundler |
| Babel | 7.x | JavaScript transpilation |
| PostCSS | 8.x | CSS transformations |
| ESLint | 8.x | JavaScript linting |
| Stylelint | 15.x | CSS/SCSS linting |
DevOps & Testing
| Technology | Purpose |
|---|---|
| Docker | Local development environment |
| NGINX | Web server |
| rsync | Deployment automation |
| Playwright | End-to-end testing |
| PHPUnit | Unit testing |
| PHPCS | PHP 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
2. Animated Gallery – Horizontal Scroll Carousel
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