WordPress powers 43% of the web, but the quality of WordPress development varies enormously. The difference between a maintainable, performant custom build and a fragile, slow one comes down to following established best practices at every layer: theme architecture, block development, PHP code quality, security, performance, and deployment.
This guide covers the current 2026 standard for each area — what expert WordPress developers do and what they avoid.
1. Theme Architecture
Block Themes vs Classic Themes in 2026
For new WordPress builds, block themes (Full Site Editing / FSE) are the modern standard. They use theme.json for design tokens and template parts instead of PHP template files:
- Use block themes for: new builds targeting WordPress 6.x, content-heavy sites, brands wanting merchant-editable layouts
- Use classic themes for: complex PHP-driven functionality, heavily customized admin workflows, sites upgrading from existing classic bases
theme.json Best Practices
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#2563eb", "name": "Primary Blue" },
{ "slug": "text", "color": "#1a1a2e", "name": "Text Dark" }
],
"custom": false,
"customDuotone": false,
"customGradient": false,
"defaultPalette": false
},
"typography": {
"fontFamilies": [
{
"slug": "inter",
"name": "Inter",
"fontFamily": "Inter, sans-serif",
"fontFace": [
{
"fontFamily": "Inter",
"fontWeight": "400 700",
"fontStyle": "normal",
"src": ["file:./assets/fonts/Inter-Variable.woff2"]
}
]
}
],
"customFontSize": false,
"defaultFontSizes": false
},
"spacing": {
"customSpacingSize": false,
"defaultSpacingSizes": false
}
}
}
Key principle: set “custom”: false and “defaultPalette”: false to lock down the editor to your defined tokens. This prevents content editors from applying arbitrary colors that break brand consistency.
2. Custom Block Development
block.json is Required
Every custom block must use block.json for registration. This enables server-side rendering metadata, script asset dependencies, and block editor API features:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "wpdev/hero-banner",
"version": "1.0.0",
"title": "Hero Banner",
"category": "theme",
"icon": "cover-image",
"supports": {
"html": false,
"align": ["wide", "full"],
"color": { "background": true, "text": true }
},
"attributes": {
"heading": { "type": "string", "default": "" },
"subheading": { "type": "string", "default": "" },
"ctaText": { "type": "string", "default": "" },
"ctaUrl": { "type": "string", "default": "" }
},
"editorScript": "file:./index.js",
"editorStyle": "file:./editor.css",
"style": "file:./style.css",
"render": "file:./render.php"
}
PHP render.php for Dynamic Blocks
<?php
// render.php — server-side rendered output
$heading = isset( $attributes['heading'] ) ? esc_html( $attributes['heading'] ) : '';
$subheading = isset( $attributes['subheading'] ) ? esc_html( $attributes['subheading'] ) : '';
$cta_text = isset( $attributes['ctaText'] ) ? esc_html( $attributes['ctaText'] ) : '';
$cta_url = isset( $attributes['ctaUrl'] ) ? esc_url( $attributes['ctaUrl'] ) : '';
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<h1 class="hero-heading"><?php echo $heading; ?></h1>
<p class="hero-subheading"><?php echo $subheading; ?></p>
<?php if ( $cta_text && $cta_url ) : ?>
<a href="<?php echo $cta_url; ?>" class="btn"><?php echo $cta_text; ?></a>
<?php endif; ?>
</div>
3. PHP Best Practices
Do
- Namespace all functions and classes
- Use Composer for autoloading
- Escape all output: esc_html(), esc_attr(), esc_url()
- Sanitize all input: sanitize_text_field(), absint(), wp_kses_post()
- Verify nonces on all form submissions and AJAX requests
- Use capability checks before sensitive operations
- Prefix all hooks and options with your unique prefix
Don’t
- Echo $_POST or $_GET values directly
- Use global function names without prefixes
- Query the database directly (use WP_Query)
- Skip nonce verification on admin AJAX
- Use eval() or extract()
- Modify core WordPress files
- Store sensitive data in wp_options without serialization care
Security: Capability Checks and Nonces
// AJAX handler example with proper security
add_action( 'wp_ajax_save_user_preference', function() {
// 1. Verify nonce
check_ajax_referer( 'save_preference_nonce', 'nonce' );
// 2. Check capability
if ( ! current_user_can( 'edit_posts' ) ) {
wp_send_json_error( 'Insufficient permissions', 403 );
}
// 3. Sanitize input
$preference = sanitize_text_field( $_POST['preference'] ?? '' );
$user_id = get_current_user_id();
// 4. Do the work
update_user_meta( $user_id, 'my_preference', $preference );
wp_send_json_success( [ 'message' => 'Saved' ] );
} );
4. Performance: Core Web Vitals
Every custom WordPress build should target these CWV scores before launch:
- LCP (Largest Contentful Paint): < 2.5s — optimize hero images with fetchpriority=”high”, no lazy loading above fold
- CLS (Cumulative Layout Shift): < 0.1 — set explicit width/height on all images and iframes
- INP (Interaction to Next Paint): < 200ms — minimize main thread blocking from large scripts
Asset Loading Best Practices
// Correct wp_enqueue_script usage
add_action( 'wp_enqueue_scripts', function() {
// Enqueue with correct dependencies and defer
wp_enqueue_script(
'my-theme-main',
get_template_directory_uri() . '/assets/js/main.js',
[], // no jQuery dependency
wp_get_theme()->get( 'Version' ),
[
'in_footer' => true,
'strategy' => 'defer', // WP 6.3+
]
);
// Inline critical CSS instead of separate file
wp_add_inline_style( 'my-theme-style', get_critical_css() );
} );
5. Custom Post Types and Taxonomies
// Register CPT with full block editor support
add_action( 'init', function() {
register_post_type( 'wpdev_project', [
'labels' => [
'name' => __( 'Projects', 'my-theme' ),
'singular_name' => __( 'Project', 'my-theme' ),
],
'public' => true,
'show_in_rest' => true, // Required for Gutenberg support
'supports' => [ 'title', 'editor', 'thumbnail', 'custom-fields' ],
'has_archive' => true,
'rewrite' => [ 'slug' => 'projects' ],
'menu_icon' => 'dashicons-portfolio',
] );
} );
6. Development Workflow
Local Development
- Local (Flywheel) — fastest setup, integrates with WP Engine/Flywheel hosting
- wp-env — official WordPress CLI tool, Docker-based, best for block development
- Lando — flexible Docker config, good for multi-service projects
Git Workflow
- Never commit wp-config.php, .env, or upload directories
- Commit composer.lock and package-lock.json
- Use feature branches, require PRs with code review before merging to main
- Tag releases with semantic versioning
Automated Deployment
# .github/workflows/deploy.yml
name: Deploy to Staging
on:
push:
branches: [develop]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Composer dependencies
run: composer install --no-dev --optimize-autoloader
- name: Build assets
run: npm ci && npm run build
- name: Deploy via rsync
uses: burnett01/rsync-deployments@7.0.1
with:
switches: -avzr --delete --exclude='.git'
path: ./
remote_path: ${{ secrets.STAGING_PATH }}
remote_host: ${{ secrets.STAGING_HOST }}
remote_user: ${{ secrets.STAGING_USER }}
remote_key: ${{ secrets.SSH_PRIVATE_KEY }}
7. Testing
- PHPUnit — unit tests for custom PHP functions and classes
- WP_UnitTestCase — WordPress-aware test base class for plugin/theme tests
- Playwright or Cypress — end-to-end browser tests for critical user flows
- PHP_CodeSniffer with WordPress standards — automated code style checks in CI
Frequently Asked Questions
Key 2026 best practices: build with block themes and theme.json for new projects, use register_block_type with block.json for custom blocks, follow PSR-4 autoloading and namespacing for PHP, implement capability checks and nonce verification for all AJAX/REST endpoints, and target Core Web Vitals (LCP < 2.5s, CLS < 0.1, INP < 200ms).
For professional WordPress development, avoid page builders (Elementor, Divi) for custom builds. They add significant page weight, create maintenance debt, and limit performance optimization. Build with native WordPress tools: block themes, custom Gutenberg blocks, and clean PHP.
Classic themes use PHP templates plus the Customizer, while block themes (Full Site Editing) define everything — templates, headers, footers, styles — through blocks and a theme.json. In 2026, block themes are the default direction for new builds: theme.json centralizes design tokens and global styles, and merchants edit layouts in the Site Editor. Classic themes remain valid for sites with heavy custom PHP or legacy requirements, but new projects should start with a block theme unless there’s a specific reason not to.
Follow WordPress Coding Standards for formatting, use namespacing to avoid collisions, implement PSR-4 autoloading via Composer, use strict types where appropriate, escape all output with esc_html()/esc_attr()/esc_url(), and sanitize all input with appropriate sanitize_*() functions.
Follow the core rules every time: escape all output (esc_html, esc_attr, esc_url), sanitize all input (sanitize_text_field, absint, wp_kses_post), verify nonces on form submissions and AJAX, and run capability checks before any sensitive operation. Never echo $_POST/$_GET directly, query the database raw instead of using WP_Query, or skip nonce verification on admin AJAX. Prefix your functions, hooks, and options to avoid collisions. These habits prevent the large majority of WordPress vulnerabilities.
Target the three metrics directly: for LCP, optimize and properly size the hero image (WebP, fetchpriority, no lazy-loading above the fold) and cut render-blocking CSS/JS; for CLS, set explicit width and height on images and reserve space for embeds; for INP, minimize and defer heavy JavaScript. Enqueue only the assets each page needs instead of loading the whole framework everywhere, and lean on caching and a CDN. Custom themes win here because you control exactly what loads.
Cover several layers: validate HTML and check accessibility, run cross-browser and real-device mobile testing, and confirm Core Web Vitals with Lighthouse. For code, use PHPUnit for unit tests and WP-CLI to script setup, plus PHP_CodeSniffer with the WordPress coding standards. Develop with WP_DEBUG on to catch notices, and verify behavior with realistic data volumes. Automate what you can in CI so regressions are caught before deploy.
Use Local (Flywheel) or Lando for local development, Git for version control, wp-env for block development environment, automated deployments via GitHub Actions or DeployHQ, and staging environments on your host for pre-launch testing.
Related Resources
Custom WordPress Development
Our full-service WordPress development — no page builders, best practices standard.
Learn MoreCustom Gutenberg Block Development
Deep dive into building reusable blocks with block.json and React.
Learn More