After eight years of WordPress development, I’ve refined my toolchain to focus on speed, reliability, and maintainability. The WordPress ecosystem has evolved dramatically, and the tools we use today are light-years ahead of what we had even three years ago. This isn’t another generic “best tools” listicle—this is my actual development stack that I use daily for client projects, custom themes, and Gutenberg block development.
The biggest shift I’ve seen is the move toward containerized development environments and AI-assisted coding workflows. These aren’t just trendy additions—they solve real problems that have plagued WordPress development for years: environment consistency, dependency management, and repetitive coding tasks.
Core Development Environment Setup
Your development environment is the foundation everything else builds on. I’ve moved completely away from MAMP/XAMPP solutions to a Docker-based approach that mirrors production environments exactly.
Docker with Local by Flywheel Alternative
While Local by Flywheel is popular, I prefer a custom Docker setup for better control and consistency. Here’s my docker-compose.yml configuration that I use for all WordPress projects:
version: '3.8'
services:
wordpress:
build:
context: .
dockerfile: Dockerfile.wordpress
ports:
- "8000:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DEBUG: 1
WORDPRESS_CONFIG_EXTRA: |
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true);
define('WP_ENVIRONMENT_TYPE', 'development');
volumes:
- ./wp-content:/var/www/html/wp-content
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
depends_on:
- db
- redis
networks:
- wp-network
db:
image: mysql:8.0
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
MYSQL_ROOT_PASSWORD: rootpassword
volumes:
- db_data:/var/lib/mysql
- ./mysql-init:/docker-entrypoint-initdb.d
networks:
- wp-network
redis:
image: redis:7-alpine
networks:
- wp-network
mailhog:
image: mailhog/mailhog:latest
ports:
- "8025:8025"
networks:
- wp-network
volumes:
db_data:
networks:
wp-network:
driver: bridge
This setup includes Redis for object caching and MailHog for email testing—two things that are essential for professional WordPress development but often overlooked in simpler setups.
VS Code Configuration for WordPress Excellence
VS Code has become the de facto standard for WordPress development, but the default setup isn’t optimized for our workflows. Here are my essential extensions and configuration:
- PHP Intelephense – Superior to the default PHP extension
- WordPress Hooks Intellisense – Autocomplete for WP hooks
- Gutenberg Blocks Snippets – Essential for block development
- Docker – Container management within VS Code
- GitLens – Advanced Git integration
- Prettier – Code formatting with WordPress standards
My VS Code settings.json for WordPress development:
{
"php.suggest.basic": false,
"intelephense.files.associations": ["*.php", "*.phtml", "*.inc", "*.module"],
"intelephense.files.exclude": [
"**/vendor/**/*.php",
"**/node_modules/**",
"**/wp-admin/**",
"**/wp-includes/**"
],
"emmet.includeLanguages": {
"javascript": "javascriptreact",
"php": "html"
},
"files.associations": {
"*.php": "php"
},
"editor.tabSize": 2,
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"prettier.tabWidth": 2,
"prettier.useTabs": true,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.tabSize": 4,
"editor.insertSpaces": false
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"wordpress.multiSiteMode": false,
"wordpress.autoCompleteEnabled": true
}
Notice how I’m using different tab sizes for PHP (4 spaces) versus JavaScript (2 spaces) to follow WordPress coding standards exactly.
Command Line Tools That Actually Matter
The command line is where WordPress development productivity really accelerates. These tools have become indispensable in my daily workflow.
WP-CLI: Beyond the Basics
Everyone knows WP-CLI exists, but most developers only scratch the surface. Here are some advanced WP-CLI workflows that save me hours every week:
#!/bin/bash
# My WordPress project setup script
# Download WordPress core without default themes/plugins
wp core download --skip-content
# Create wp-config with environment-specific constants
wp config create --dbname=wordpress --dbuser=wordpress --dbpass=wordpress --dbhost=db --extra-php <<PHP
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SCRIPT_DEBUG', true);
define('WP_CACHE', true);
define('WP_REDIS_HOST', 'redis');
define('WP_ENVIRONMENT_TYPE', 'development');
// Disable file editing in admin
define('DISALLOW_FILE_EDIT', true);
// Increase memory limit for development
define('WP_MEMORY_LIMIT', '512M');
PHP
# Install WordPress
wp core install --url=localhost:8000 --title="Dev Site" --admin_user=admin --admin_password=password --admin_email=dev@example.com
# Install essential plugins
wp plugin install redis-cache query-monitor debug-bar --activate
# Remove default content
wp post delete 1 2 --force
wp comment delete 1
# Create custom post types for testing
wp post-type register project --public=true --has_archive=true
# Set up pretty permalinks
wp rewrite structure '/%postname%/' --hard
# Enable Redis caching
wp redis enable
echo "WordPress development environment ready!"
I save this as a script and run it every time I start a new project. The key insight here is that wp-config.php generation through WP-CLI allows for environment-specific constants that are consistent across team members.
Composer for WordPress Dependency Management
Composer isn’t just for framework development—it’s essential for modern WordPress projects. Here’s how I structure my composer.json for WordPress development:
{
"name": "your-name/wordpress-project",
"description": "Modern WordPress development setup",
"type": "project",
"require": {
"php": ">=8.0",
"johnpbloch/wordpress-core": "^6.4",
"wpackagist-plugin/redis-cache": "^2.5",
"wpackagist-theme/twentytwentyfour": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"wp-coding-standards/wpcs": "^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"phpstan/phpstan": "^1.10",
"szepeviktor/phpstan-wordpress": "^1.3",
"wpackagist-plugin/query-monitor": "^3.12"
},
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org"
}
],
"extra": {
"wordpress-install-dir": "web/wp",
"installer-paths": {
"web/wp-content/plugins/{$name}/": ["type:wordpress-plugin"],
"web/wp-content/themes/{$name}/": ["type:wordpress-theme"]
}
},
"scripts": {
"lint": "phpcs --standard=WordPress --extensions=php .",
"lint:fix": "phpcbf --standard=WordPress --extensions=php .",
"analyze": "phpstan analyze --level=5 wp-content/themes/ wp-content/plugins/",
"test": "phpunit"
},
"autoload": {
"psr-4": {
"YourNamespace\": "web/wp-content/themes/your-theme/inc/"
}
}
}
This setup gives you WordPress core management, plugin/theme dependency management, code quality tools, and PSR-4 autoloading for your custom theme or plugin code. The script shortcuts make code quality checks effortless.
Modern Build Tools and Asset Management
The days of manually concatenating CSS files and optimizing images one by one are long gone. Modern WordPress development requires sophisticated build tooling.
WP-Rig vs Custom Webpack Configurations
I’ve used both approaches extensively. WP-Rig is excellent for getting started quickly, but for complex projects, I prefer a custom Webpack setup that gives me complete control. Here’s my production-ready webpack.config.js:
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
entry: {
'theme': './assets/src/js/theme.js',
'admin': './assets/src/js/admin.js',
'gutenberg': './assets/src/js/gutenberg.js',
'style': './assets/src/scss/style.scss',
'admin-style': './assets/src/scss/admin.scss'
},
output: {
path: path.resolve(__dirname, 'assets/dist'),
filename: 'js/[name].[contenthash].js',
clean: true
},
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: 'defaults' }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
}
},
{
test: /.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
},
{
test: /.(png|jpg|jpeg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8192
}
},
generator: {
filename: 'images/[name].[contenthash][ext]'
}
}
]
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
}),
!isProduction && new BrowserSyncPlugin({
proxy: 'localhost:8000',
port: 3000,
files: [
'**/*.php',
'assets/dist/**/*'
],
notify: false
})
].filter(Boolean),
optimization: {
minimizer: [
new TerserPlugin(),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
filename: 'js/vendors.[contenthash].js'
}
}
}
},
devtool: isProduction ? false : 'source-map',
externals: {
jquery: 'jQuery',
react: 'React',
'react-dom': 'ReactDOM'
}
};
This configuration handles React JSX for Gutenberg blocks, SCSS compilation, asset optimization, and BrowserSync integration. The externals section is crucial—it prevents bundling WordPress’s built-in libraries, keeping your bundle sizes small.
Asset Enqueuing with Cache Busting
Webpack generates files with content hashes, but WordPress needs to know about them. Here’s how I handle dynamic asset enqueuing:
asset_dir = get_template_directory() . '/assets/dist';
$this->load_manifest();
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
add_action('enqueue_block_editor_assets', [$this, 'enqueue_gutenberg_assets']);
}
private function load_manifest() {
$manifest_path = $this->asset_dir . '/manifest.json';
if (file_exists($manifest_path)) {
$this->manifest = json_decode(file_get_contents($manifest_path), true);
} else {
$this->manifest = [];
}
}
private function get_asset_url($asset_key) {
if (isset($this->manifest[$asset_key])) {
return get_template_directory_uri() . '/assets/dist/' . $this->manifest[$asset_key];
}
// Fallback for development
return get_template_directory_uri() . '/assets/dist/' . $asset_key;
}
public function enqueue_frontend_assets() {
// Enqueue main theme styles
wp_enqueue_style(
'theme-style',
$this->get_asset_url('css/style.css'),
[],
null // Version is handled by content hash
);
// Enqueue main theme script
wp_enqueue_script(
'theme-script',
$this->get_asset_url('js/theme.js'),
['jquery'],
null,
true
);
// Localize script with AJAX URL and nonce
wp_localize_script('theme-script', 'themeAjax', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('theme_nonce'),
'apiUrl' => rest_url('wp/v2/')
]);
}
public function enqueue_admin_assets($hook) {
// Only load on specific admin pages
$allowed_pages = ['post.php', 'post-new.php', 'edit.php'];
if (in_array($hook, $allowed_pages)) {
wp_enqueue_style(
'theme-admin-style',
$this->get_asset_url('css/admin-style.css'),
[],
null
);
wp_enqueue_script(
'theme-admin-script',
$this->get_asset_url('js/admin.js'),
['jquery'],
null,
true
);
}
}
public function enqueue_gutenberg_assets() {
wp_enqueue_script(
'theme-gutenberg',
$this->get_asset_url('js/gutenberg.js'),
['wp-blocks', 'wp-element', 'wp-editor'],
null,
true
);
wp_enqueue_style(
'theme-gutenberg-style',
$this->get_asset_url('css/gutenberg.css'),
['wp-edit-blocks'],
null
);
}
}
// Initialize asset manager
new AssetManager();
This approach automatically handles cache busting through webpack’s content hashes and ensures assets are only loaded where needed. The manifest.json file is generated by webpack and contains the mapping between your source file names and the generated files with hashes.
Testing and Quality Assurance Tools
Testing WordPress applications used to be an afterthought, but it’s become essential for maintaining quality as projects grow in complexity.
Playwright for End-to-End Testing
Playwright has become my go-to tool for WordPress E2E testing. It’s faster and more reliable than Selenium-based solutions. Here’s a real-world test suite I use for WordPress projects:
// tests/wordpress-admin.spec.js
const { test, expect } = require('@playwright/test');
// WordPress admin authentication
test.beforeEach(async ({ page }) => {
await page.goto('/wp-admin');
await page.fill('#user_login', 'admin');
await page.fill('#user_pass', 'password');
await page.click('#wp-submit');
await expect(page).toHaveURL(/wp-admin/);
});
test('Create and publish a blog post', async ({ page }) => {
// Navigate to new post
await page.click('text=Posts');
await page.click('text=Add New');
// Wait for Gutenberg to load
await page.waitForSelector('.editor-post-title__input');
// Add post title
await page.fill('.editor-post-title__input', 'Test Post from Playwright');
// Add content block
await page.click('.editor-default-block-appender__content');
await page.type('.editor-default-block-appender__content', 'This is test content created by Playwright automation.');
// Publish post
await page.click('text=Publish');
await page.click('.editor-post-publish-panel__header-publish-button button');
// Verify publication
await expect(page.locator('.editor-post-publish-panel__header-published')).toContainText('Published');
// Visit post on frontend
await page.click('text=View Post');
await expect(page.locator('h1')).toContainText('Test Post from Playwright');
});
test('Custom block functionality', async ({ page }) => {
await page.goto('/wp-admin/post-new.php');
await page.waitForSelector('.editor-post-title__input');
// Add custom block
await page.click('.editor-inserter-button');
await page.type('.block-editor-inserter__search-input', 'Custom CTA Block');
await page.click('text=Custom CTA Block');
// Configure block settings
await page.fill('[placeholder="Enter CTA title"]', 'Test CTA Title');
await page.fill('[placeholder="Enter CTA description"]', 'This is a test CTA description');
await page.fill('[placeholder="Button text"]', 'Click Here');
// Switch to preview mode
await page.click('text=Preview');
await page.click('text=Preview in new tab');
// Verify block renders correctly on frontend
const [newPage] = await Promise.all([
page.waitForEvent('popup'),
page.click('text=Preview in new tab')
]);
await expect(newPage.locator('.cta-block h3')).toContainText('Test CTA Title');
await expect(newPage.locator('.cta-block .button')).toContainText('Click Here');
await newPage.close();
});
test('Plugin activation and deactivation', async ({ page }) => {
await page.goto('/wp-admin/plugins.php');
// Activate plugin
const pluginRow = page.locator('tr[data-plugin="query-monitor/query-monitor.php"]');
if (await pluginRow.locator('text=Activate').isVisible()) {
await pluginRow.locator('text=Activate').click();
await expect(page.locator('.notice-success')).toContainText('Plugin activated');
}
// Verify plugin is active
await expect(pluginRow.locator('text=Deactivate')).toBeVisible();
// Check that Query Monitor appears in admin bar
await page.goto('/wp-admin');
await expect(page.locator('#wp-admin-bar-query-monitor')).toBeVisible();
});
These tests cover the most common WordPress workflows: content creation, custom block functionality, and plugin management. The key is testing user workflows, not just individual components.
Code Quality and Static Analysis
Manual code review catches some issues, but automated tools catch the rest. Here’s my phpcs.xml configuration for WordPress projects:
Custom coding standards for WordPress development
wp-content/themes/
wp-content/plugins/
wp-content/mu-plugins/
*/vendor/*
*/node_modules/*
*/tests/*
*/build/*
*/dist/*
7.5
I run this as part of my pre-commit hooks using Husky, so code quality issues are caught before they enter version control.
AI-Powered Development Workflow Integration
AI tools have fundamentally changed how I approach WordPress development. They’re not replacements for knowledge and experience—they’re force multipliers that handle repetitive tasks and provide intelligent suggestions.
GitHub Copilot for WordPress Development
Copilot works exceptionally well with WordPress once you train it with good context. I maintain custom snippets that provide context for common WordPress patterns:
- Hook Documentation: I include comments with hook names and parameters
- Function Prefixing: Consistent function naming helps Copilot suggest appropriate prefixes
- Custom Post Type Patterns: Copilot learns your CPT registration patterns quickly
- Security Patterns: It’s excellent at suggesting proper nonce verification and data sanitization
Claude for Complex Problem Solving
For architectural decisions and complex debugging, I use Claude with detailed context about the project structure. The key is providing comprehensive information about your WordPress setup, active plugins, and specific requirements.
Performance Monitoring and Debugging
Performance monitoring isn’t just for production—development environments need robust debugging tools to catch issues early.
Query Monitor Configuration
Query Monitor is essential, but most developers don’t configure it properly for maximum effectiveness. Here’s my setup:
- Database Queries: Set
SAVEQUERIESto true in wp-config.php - HTTP Requests: Monitor external API calls and their performance
- Hook Analysis: Track which plugins are adding hooks and their execution time
- Template Debugging: Identify which template files are loaded for each request
Redis Object Caching Setup
Object caching should be part of every development environment. My Redis configuration catches caching issues before they reach production:
<?php
// wp-content/object-cache.php configuration
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_PREFIX', 'wp_dev');
// Enable Redis debugging in development
if (defined('WP_DEBUG') && WP_DEBUG) {
define('WP_REDIS_DISABLE_BANNERS', false);
define('WP_REDIS_DISABLE_METRICS', false);
define('WP_REDIS_DISABLE_ADMIN_BAR', false);
}
// Cache key compression
define('WP_REDIS_COMPRESSION', 'zstd');
// Ignore certain cache groups in development
$redis_ignored_groups = [
'counts',
'plugins',
'themes'
];
This configuration provides detailed caching metrics during development while maintaining performance benefits.
Key Takeaways for Modern WordPress Development
The WordPress development landscape has matured significantly, and the tools available today can dramatically improve your productivity and code quality. Here are the essential points to remember:
- Containerization is non-negotiable: Docker setups ensure consistency across development environments and eliminate “it works on my machine” problems
- Build tools should match project complexity: Use WP-Rig for simple themes, custom Webpack for complex applications with multiple entry points
- Testing saves time long-term: Playwright tests catch regressions faster than manual testing ever could
- Code quality tools prevent technical debt: PHPCS and PHPStan catch issues before they become expensive problems
- AI tools amplify expertise: They’re most effective when you already understand WordPress patterns and best practices
The biggest mistake I see developers make is trying to adopt every new tool at once. Start with Docker for environment consistency, add build tools for asset management, then gradually introduce testing and AI assistance. Each tool should solve a specific problem you’re experiencing, not just be the latest trending technology.
This toolchain has evolved through years of real client work, failed experiments, and successful optimizations. The key is finding the balance between sophistication and simplicity—your tools should accelerate development, not become a maintenance burden themselves.
