Category: Dev Tips & Snippets

  • Understanding the WordPress Request Lifecycle — A Deep Dive with Diagrams

    Every time someone visits a WordPress site, a precise sequence of events fires behind the scenes. Understanding this lifecycle is the key to writing better themes, plugins, and custom solutions. Let’s trace it from the very first byte to the final rendered page.


    Table of Contents

    1. The Big Picture
    2. Phase 1 — Bootstrap & Environment Setup
    3. Phase 2 — Core Loading & Plugin Initialization
    4. Phase 3 — Query Parsing & The Main Query
    5. Phase 4 — Template Resolution & Rendering
    6. Phase 5 — Shutdown & Cleanup
    7. Key Hooks Quick Reference
    8. How the Admin (wp-admin) Lifecycle Differs
    9. How the REST API Lifecycle Differs
    10. Practical Tips for Plugin & Theme Developers

    The Big Picture

    Before we get into the details, let’s zoom out. A WordPress page request follows five distinct phases: Bootstrap, Core Loading, Query Parsing, Template Resolution, and Shutdown. Each phase fires a set of action and filter hooks that give developers insertion points to modify behavior.

    Here’s the complete lifecycle at a glance:

    Now let’s walk through each phase in detail.


    Phase 1 — Bootstrap & Environment Setup

    Everything starts at index.php. This file is tiny — it simply defines WP_USE_THEMES as true and loads wp-blog-header.php. From there, the bootstrap chain begins:

    WordPress bootstrap chain diagram showing index.php to wp-blog-header.php to wp-load.php to wp-config.php to wp-settings.php

    What happens inside wp-settings.php

    This is where the heavy lifting begins. WordPress loads its essential libraries in a specific order. It sets up error handling, initializes the global $wpdb database object, establishes the object cache (which means if you have an external object cache like Redis or Memcached, it kicks in here), and loads the core translation files.

    Some key constants set during this phase include ABSPATH, WPINC, and WP_CONTENT_DIR. If you’ve ever defined WP_DEBUG or WP_CACHE in your wp-config.php, this is where those constants get picked up and applied.

    💡 Developer Tip:

    If you need to run code before WordPress fully loads — for example, to set up a custom caching drop-in — you can place a file at wp-content/advanced-cache.php and set WP_CACHE to true in wp-config.php. This file is loaded extremely early, even before plugins.


    Phase 2 — Core Loading & Plugin Initialization

    Once the environment is ready, WordPress enters its core loading phase. This is where your code — plugins and themes — comes alive. The order is strict and intentional:

    WordPress plugin and theme loading order diagram showing MU plugins, plugins_loaded, theme functions.php, init, and wp_loaded hooks

    MU Plugins vs Regular Plugins

    Must-Use plugins (dropped into wp-content/mu-plugins/) load before regular plugins and cannot be deactivated through the admin UI. This makes them ideal for site-critical functionality like custom authentication, security rules, or performance optimizations that must always be active. They also load in alphabetical order by filename, with no guaranteed dependency resolution — keep that in mind if one MU plugin depends on another.

    The plugins_loaded Hook

    By the time plugins_loaded fires, all active plugins have been included. This is the earliest safe hook for plugin-to-plugin communication. If your plugin needs to check whether another plugin (like WooCommerce or Gravity Forms) is active, this is where you do it.

    The init Hook

    The init hook is arguably the most important hook in WordPress. By this point, the current user is authenticated, the locale is set, and you have full access to the WordPress API. This is the standard place to register custom post types, taxonomies, shortcodes, blocks, and REST API routes.

    // Register a custom post type at the right time
    add_action( 'init', function() {
    register_post_type( 'portfolio', [
    'label' => 'Portfolio',
    'public' => true,
    'show_in_rest' => true,
    'supports' => [ 'title', 'editor', 'thumbnail' ],
    ] );
    } );

    Phase 3 — Query Parsing & The Main Query

    With everything loaded, WordPress now turns its attention to figuring out what the visitor actually requested. This is where the URL gets translated into a database query.

    WordPress query parsing flowchart showing URL rewrite matching, parse_request, pre_get_posts, WP_Query execution, and wp hook

    Rewrite Rules

    WordPress maintains a table of rewrite rules (stored in the rewrite_rules option) that map URL patterns to query variables. When a request comes in, WP::parse_request() iterates through these rules using regex matching until it finds a match. The matched rule translates the pretty permalink into internal query vars like name=my-post or category_name=tutorials.

    pre_get_posts — The Power Hook

    The pre_get_posts action fires just before the main query executes. This is the correct way to modify the main query. Do not modify it through query_posts(). It creates a new query and can cause pagination bugs and performance issues.

    // Show 20 posts per page on the blog archive
    add_action( 'pre_get_posts', function( $query ) {
    if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
    $query->set( 'posts_per_page', 20 );
    }
    } );

    ⚠️ Common Mistake:

    Always check is_main_query() inside your pre_get_posts callback. Without it, you’ll accidentally modify every WP_Query on the page. This includes sidebar widgets, related posts, and any custom queries in your theme.


    Phase 4 — Template Resolution & Rendering

    With the query results ready, WordPress now needs to decide which template file to use. The WordPress Template Hierarchy governs this decision. It is a well-defined decision tree that goes from the most specific template to the most generic.

    WordPress template hierarchy diagram showing how single posts, pages, archives, categories, search, and 404 requests resolve to template files

    template_redirect

    The template_redirect hook fires before any template file is loaded. It’s your last opportunity to redirect users — for example, redirecting non-logged-in users away from a members-only page. If you call wp_redirect() here followed by exit, no template file is loaded at all.

    The Loop

    Once the template file is loaded, the output is rendered using The Loop — WordPress’s core mechanism for iterating over query results:

    if ( have_posts() ) :
    while ( have_posts() ) : the_post();
    the_title( '<h2>', '</h2>' );
    the_content();
    endwhile;
    endif;

    Each call to the_post() advances the internal pointer. It sets up the global $post object. This setup is why template tags like the_title() and the_content() work without explicitly passing a post ID.


    Phase 5 — Shutdown & Cleanup

    After the full HTML response has been generated and sent to the browser, WordPress fires its shutdown sequence.

    WordPress shutdown phase diagram showing HTML sent to browser, shutdown hook, DB connection close, and WP-Cron spawn

    The shutdown hook is registered via PHP’s register_shutdown_function(). It fires even if a fatal error occurs (in PHP 7+), making it useful for logging, cleanup, or sending data to external services without affecting page load time. WordPress also uses this moment to potentially spawn a WP-Cron request if there are scheduled tasks pending.

    💡 Performance Note:

    WP-Cron runs as an HTTP “loopback” request by default, triggered on page load. For production sites, it’s best to disable this with define('DISABLE_WP_CRON', true); in wp-config.php and set up a real server cron job instead.


    Key Hooks Quick Reference

    Here’s a quick reference of the most important hooks in the lifecycle, in the order they fire:

    HookTypeWhen It FiresCommon Use
    muplugins_loadedActionAfter MU plugins loadMU plugin initialization
    plugins_loadedActionAfter all plugins loadPlugin-to-plugin communication, textdomains
    after_setup_themeActionAfter theme’s functions.phpTheme supports, image sizes, nav menus
    initActionWordPress fully loadedCPTs, taxonomies, shortcodes, blocks, REST routes
    wp_loadedActionWP loaded, before queryForm processing, early redirects
    parse_requestActionURL matched to query varsCustom URL handling
    pre_get_postsActionBefore main query runsModify queries (posts per page, ordering)
    wpActionQuery done, headers setAccess query results before template loads
    template_redirectActionBefore template loadsConditional redirects, access control
    wp_enqueue_scriptsActionDuring template loadEnqueue CSS/JS files
    wp_headActionInside <head>Meta tags, inline styles/scripts
    wp_footerActionBefore </body>Deferred scripts, tracking codes
    shutdownActionAfter response sentLogging, analytics, cleanup

    How the Admin (wp-admin) Lifecycle Differs

    The admin dashboard follows the same bootstrap and loading phases, but diverges after init. Instead of parsing a URL into a post query, WordPress loads the admin framework:

    WordPress admin lifecycle diagram showing divergence after init hook to admin.php, admin_init, admin_menu, and admin_enqueue_scripts

    The key difference is that admin_init fires instead of the frontend query hooks. This is the right hook for registering settings, running admin-only logic, and processing form submissions in the backend. Similarly, use admin_enqueue_scripts (not wp_enqueue_scripts) to load CSS and JS only on admin pages.


    How the REST API Lifecycle Differs

    REST API requests follow the bootstrap and plugin loading phases, but then branch off to the REST server instead of the template system:

    WordPress REST API lifecycle flowchart showing rest_api_init, route matching, permission_callback authorization check, and JSON response

    There’s no template hierarchy, no Loop, and no wp_head/wp_footer. The response is pure JSON. Routes are registered on the rest_api_init hook using register_rest_route(), and each route defines its own permission callback and handler. The REST server handles serialization, status codes, and error formatting.


    Practical Tips for Plugin & Theme Developers

    1. Hook at the Right Time

    One of the most common mistakes is hooking too early or too late. If you register a custom post type on plugins_loaded instead of init, rewrite rules won’t be generated properly. If you try to enqueue scripts on init instead of wp_enqueue_scripts, they might not be added to the page correctly. Refer to the hooks table above and match your action to the right moment.

    2. Never Use query_posts()

    This function creates a new WP_Query and overwrites the global $wp_query, breaking pagination and conditional tags. Use pre_get_posts to modify the main query, or create a new WP_Query instance for secondary loops.

    3. Understand Loading Order for Dependencies

    If your plugin depends on another plugin, check for its existence on plugins_loaded (not on init or at file level). MU plugins load before regular plugins, and themes load after plugins. This order matters when you’re extending another developer’s code.

    4. Use template_redirect for Access Control

    Don’t check user permissions inside template files. Use template_redirect to redirect unauthorized users before the template is even loaded. This is cleaner and avoids partial HTML output.

    5. Leverage shutdown for Non-Blocking Work

    If you need to send data to an analytics service or run a cleanup task, hook into shutdown. The response has already been sent to the browser, so this work doesn’t add to the perceived page load time. Pair this with fastcgi_finish_request() on supported servers for true async behavior.


    Wrapping Up

    The WordPress lifecycle might seem complex at first, but it follows a logical, predictable flow. Once you internalize the order of hooks, you’ll write plugins and themes that are more robust. You’ll also write code that is more performant and maintainable. Bookmark the hooks table and diagrams above — they’ll save you hours of debugging.

    Got questions or spotted something I missed? Drop me a line — I’d love to hear from fellow WordPress developers.

  • How to Build a WP-CLI Custom Command in a Plugin (With Arguments + Progress Bar)

    How to Build a WP-CLI Custom Command in a Plugin (With Arguments + Progress Bar)

    If you’ve ever found yourself running repetitive admin tasks through the WordPress dashboard — bulk-updating post meta, cleaning up orphaned data, triggering migrations — there’s a better way. You can build a WP-CLI custom command directly inside your plugin and run it from the terminal in seconds.

    This guide walks you through the full process: registering a command, handling arguments and options, and adding a progress bar so you know exactly where things stand during long operations.

    TL;DR

    • Use WP_CLI::add_command() to register a custom command inside your plugin.
    • Define a class with an __invoke method (for simple commands) or named methods (for subcommands).
    • Accept positional arguments and associative options via the method’s $args and $assoc_args parameters.
    • Use \WP_CLI\Utils\make_progress_bar() to show progress during batch operations.
    • Always guard your CLI code with defined( 'WP_CLI' ) && WP_CLI so it only loads in CLI context.

    Why Build a WP-CLI Custom Command?

    The WordPress admin UI is fine for one-off tasks. But when you need to process thousands of posts, run a data migration, or automate something in a deployment pipeline, clicking buttons doesn’t scale.

    A WP-CLI custom command lets you:

    • Automate repetitive tasks from the terminal or CI/CD.
    • Process large datasets without hitting PHP timeout limits.
    • Ship developer-facing tools alongside your plugin.
    • Keep dangerous operations out of the admin UI entirely.

    If your plugin does anything non-trivial with data, it probably deserves a CLI command.


    Prerequisites

    Before you start, make sure you have:

    • A working WordPress installation with WP-CLI installed.
    • A plugin (even a simple one) where you’ll add the command.
    • Familiarity with PHP classes and basic terminal usage.

    Verify WP-CLI is working:

    wp --version

    You should see something like WP-CLI 2.x.x.


    Step 1: Register Your Command With WP_CLI::add_command

    The entry point for any WP-CLI custom command is WP_CLI::add_command(). This function maps a command name to a PHP class or callable.

    Create a file in your plugin — for example, includes/class-cli-commands.php:

    <?php
    /**
    * WP-CLI commands for My Plugin.
    */
    if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
    return;
    }
    class My_Plugin_CLI_Command {
    /**
    * Greets the user.
    *
    * ## EXAMPLES
    *
    * wp myplugin greet Ankit
    *
    * @when after_wp_load
    */
    public function greet( $args, $assoc_args ) {
    $name = $args[0] ?? 'World';
    WP_CLI::success( "Hello, {$name}!" );
    }
    }
    WP_CLI::add_command( 'myplugin', 'My_Plugin_CLI_Command' );

    Then load this file from your main plugin file:

    // my-plugin.php
    if ( defined( 'WP_CLI' ) && WP_CLI ) {
    require_once __DIR__ . '/includes/class-cli-commands.php';
    }

    Now test it:

    wp myplugin greet Ankit
    # Output: Success: Hello, Ankit!

    That’s the basic pattern. The class method name (greet) becomes the subcommand. The first argument to WP_CLI::add_command() (myplugin) is the top-level namespace.

    [Internal Link: “Getting started with WordPress plugin development” -> /wordpress-plugin-development-guide/]


    Step 2: Handle WP-CLI Arguments and Options

    Every WP-CLI command method receives two parameters:

    • $args — an indexed array of positional arguments.
    • $assoc_args — an associative array of named options (flags).

    Here’s how users pass them:

    wp myplugin process 42 --dry-run --batch-size=100

    In this example:

    • $args[0] is 42 (positional).
    • $assoc_args['dry-run'] is true (flag).
    • $assoc_args['batch-size'] is 100 (named option).

    Document Your Arguments With PHPDoc

    WP-CLI parses a special docblock format to generate help text and validate input. This is strongly recommended:

    /**
    * Processes posts by type.
    *
    * ## OPTIONS
    *
    * <post_type>
    * : The post type to process.
    *
    * [--batch-size=<number>]
    * : How many posts to process per batch. Default: 50.
    *
    * [--dry-run]
    * : Preview changes without writing to the database.
    *
    * ## EXAMPLES
    *
    * wp myplugin process page --batch-size=100
    * wp myplugin process post --dry-run
    *
    * @when after_wp_load
    */
    public function process( $args, $assoc_args ) {
    $post_type = $args[0];
    $batch_size = (int) ( $assoc_args['batch-size'] ?? 50 );
    $dry_run = isset( $assoc_args['dry-run'] );
    WP_CLI::log( "Processing {$post_type} posts in batches of {$batch_size}..." );
    if ( $dry_run ) {
    WP_CLI::warning( 'Dry run enabled. No changes will be saved.' );
    }
    // Your logic here...
    }

    Now wp help myplugin process shows full usage documentation — automatically.

    Angle brackets (<post_type>) mark required arguments. Square brackets ([--batch-size=<number>]) mark optional ones. This is standard WP-CLI synopsis format.


    Step 3: Add a WP-CLI Progress Bar

    When your command processes hundreds or thousands of items, a progress bar tells the user what’s happening instead of leaving them staring at a frozen terminal.

    WP-CLI ships with a built-in helper: \WP_CLI\Utils\make_progress_bar().

    Here’s a complete example that ties everything together:

    /**
    * Regenerates SEO meta for all posts of a given type.
    *
    * ## OPTIONS
    *
    * <post_type>
    * : The post type to process.
    *
    * [--batch-size=<number>]
    * : Posts per batch. Default: 50.
    *
    * [--dry-run]
    * : Run without saving changes.
    *
    * ## EXAMPLES
    *
    * wp myplugin regenerate-meta post
    * wp myplugin regenerate-meta page --batch-size=200 --dry-run
    *
    * @when after_wp_load
    */
    public function regenerate_meta( $args, $assoc_args ) {
    $post_type = $args[0];
    $batch_size = (int) ( $assoc_args['batch-size'] ?? 50 );
    $dry_run = isset( $assoc_args['dry-run'] );
    // Count total posts.
    $count_query = new WP_Query( [
    'post_type' => $post_type,
    'post_status' => 'publish',
    'posts_per_page' => -1,
    'fields' => 'ids',
    'no_found_rows' => true,
    ] );
    $post_ids = $count_query->posts;
    $total = count( $post_ids );
    if ( 0 === $total ) {
    WP_CLI::warning( "No published {$post_type} posts found." );
    return;
    }
    WP_CLI::log( "Found {$total} posts. Processing..." );
    if ( $dry_run ) {
    WP_CLI::warning( 'Dry run enabled.' );
    }
    // Create the progress bar.
    $progress = \WP_CLI\Utils\make_progress_bar( "Regenerating meta", $total );
    $updated = 0;
    foreach ( $post_ids as $post_id ) {
    if ( ! $dry_run ) {
    // Your actual processing logic.
    $meta_value = generate_seo_meta_for( $post_id );
    update_post_meta( $post_id, '_seo_description', $meta_value );
    $updated++;
    }
    $progress->tick();
    }
    $progress->finish();
    WP_CLI::success( "Done. {$updated} posts updated." );
    }

    When you run this, you get a clean progress bar in the terminal:

    Regenerating meta 50% [========================> ] 250/500

    Progress Bar Tips

    • Always call $progress->finish() when done — even if you exit early.
    • The first argument to make_progress_bar() is the label shown to the left.
    • For very large datasets, process in batches using LIMIT/OFFSET queries instead of loading all IDs into memory at once.

    Step 4: Register Multiple Subcommands

    A single class can hold many subcommands. Each public method becomes a subcommand:

    class My_Plugin_CLI_Command {
    public function greet( $args, $assoc_args ) { /* ... */ }
    public function process( $args, $assoc_args ) { /* ... */ }
    public function regenerate_meta( $args, $assoc_args ) { /* ... */ }
    public function cleanup( $args, $assoc_args ) { /* ... */ }
    }
    WP_CLI::add_command( 'myplugin', 'My_Plugin_CLI_Command' );

    This gives you:

    wp myplugin greet
    wp myplugin process
    wp myplugin regenerate-meta
    wp myplugin cleanup

    Note: underscores in method names are automatically converted to hyphens in the CLI.

    [Internal Link: “Organizing plugin code with PHP classes” -> /wordpress-plugin-php-classes/]


    Common Mistakes

    1. Not guarding CLI code with the WP_CLI constant.
    If you skip the defined( 'WP_CLI' ) && WP_CLI check, your CLI class file will throw fatal errors on normal web requests when WP-CLI isn’t loaded.

    2. Using echo instead of WP_CLI::log().
    Always use WP-CLI’s output methods (WP_CLI::log(), WP_CLI::success(), WP_CLI::warning(), WP_CLI::error()). They respect --quiet and --format flags. Plain echo doesn’t.

    3. Calling WP_CLI::error() without understanding it halts execution.
    WP_CLI::error() prints the message and exits with code 1. It does not return. If you need a non-fatal warning, use WP_CLI::warning() instead.

    4. Loading all posts into memory at once.
    For large sites, querying all post IDs into a single array can exhaust memory. Use --batch-size with offset-based queries for production commands.

    5. Forgetting the @when after_wp_load annotation.
    Without this, your command may run before WordPress is fully loaded, causing undefined function errors for things like get_posts() or update_post_meta().

    6. Not adding docblock synopsis for arguments.
    Without the ## OPTIONS docblock, WP-CLI can’t validate input or generate help text. Users won’t know what arguments your command accepts.


    FAQ

    Can I add a WP-CLI command without a plugin?

    Yes. You can add commands via a wp-cli.yml file or a custom PHP file loaded with --require. But for distributable tools, packaging commands inside a plugin is the standard approach.

    How do I make an argument required?

    Use angle brackets in the docblock synopsis: <post_type>. WP-CLI will show an error if the user omits it.

    Can I use WP-CLI commands in cron jobs or CI/CD?

    Absolutely. That’s one of the biggest advantages. Just call wp myplugin your-command in your shell script or pipeline. Add --quiet to suppress non-error output.

    Does the progress bar work in non-interactive environments?

    WP-CLI detects non-interactive terminals (like piped output or CI logs) and degrades gracefully. The progress bar won’t break anything — it just won’t render the animated bar.

    How do I test my WP-CLI command?

    WP-CLI has a testing framework based on Behat. For simpler setups, you can write PHPUnit tests that call your command class methods directly, passing mock $args and $assoc_args arrays.

    Can I use namespaced classes with WP_CLI::add_command?

    Yes. Pass the fully qualified class name as a string: WP_CLI::add_command( 'myplugin', 'MyPlugin\\CLI\\Commands' );


    Checklist

    [ ] WP-CLI is installed and working (wp --version).

    [ ] CLI class file is guarded with defined( 'WP_CLI' ) && WP_CLI.

    [ ] CLI file is loaded conditionally in your main plugin file.

    [ ] Command is registered with WP_CLI::add_command().

    [ ] Each subcommand method accepts $args and $assoc_args.

    [ ] Arguments and options are documented in the PHPDoc ## OPTIONS block.

    [ ] @when after_wp_load is set for commands that use WordPress functions.

    [ ] Long-running commands include a progress bar via make_progress_bar().

    [ ] Output uses WP_CLI::log(), WP_CLI::success(), etc. — not echo.

    [ ] Command tested locally with wp myplugin <subcommand> --help.

  • WP-CLI for Developers: Automating WordPress Tasks the Right Way

    If you’re still doing routine WordPress tasks through wp-admin, you’re leaving speed, safety, and scalability on the table.

    For modern WordPress developers, WP-CLI is not optional anymore — it’s the foundation of an automation-first workflow.

    This article explains how and why I use WP-CLI in real projects, not as a tutorial, but as a system.


    Why WP-CLI Changes Everything for Developers

    wp-admin is designed for:

    • site owners
    • editors
    • one-off actions

    WP-CLI is designed for:

    • developers
    • automation
    • repeatable workflows

    Once you cross more than one site, wp-admin stops scaling.
    WP-CLI starts shining.


    My Core Rule

    If a task is repeatable, it should not require a browser.

    That rule alone eliminated hours of manual work every month.


    Real WP-CLI Workflows I Use Weekly

    1. Environment Setup in Minutes

    Instead of clicking:

    • creating users
    • configuring URLs
    • activating plugins

    I rely on:

    • scripted installs
    • predefined plugin sets
    • automated config changes

    This ensures:

    • consistency across environments
    • zero missed steps
    • faster onboarding

    2. Safe Search & Replace (No phpMyAdmin)

    Database operations are risky when done manually.

    With WP-CLI:

    • search & replace is predictable
    • dry-runs are possible
    • mistakes are reversible

    This turns a “dangerous task” into a routine operation.


    3. Plugin & Theme Management at Scale

    Manually activating plugins across sites does not scale.

    WP-CLI allows:

    • bulk activation/deactivation
    • scripted updates
    • environment-specific logic

    This is especially powerful when:

    • managing multiple client sites
    • maintaining your own products
    • preparing releases

    4. Debugging Without Guessing

    Instead of “let’s try disabling plugins”:

    • I inspect active plugins
    • check configs
    • isolate issues faster

    WP-CLI removes random trial-and-error from debugging.


    WP-CLI + Automation = Systems, Not Tricks

    WP-CLI is most powerful when combined with:

    • shell scripts
    • CI pipelines
    • repeatable checklists

    This allows:

    • predictable deployments
    • safer updates
    • fewer production surprises

    You stop reacting and start controlling the system.


    Where AI Fits Here (Important Perspective)

    AI doesn’t replace WP-CLI.
    It accelerates how you use it.

    I use AI to:

    • generate scripts faster
    • explain unfamiliar commands
    • refactor repetitive logic

    But the workflow remains automation-first.

    Tools change.
    Systems last.


    Why WP-CLI Is Future-Ready

    WordPress is moving toward:

    • tooling
    • structured workflows
    • professional development practices

    WP-CLI aligns perfectly with that direction.

    Developers who ignore it will:

    • work slower
    • break more things
    • burn out faster

    Final Thought

    If you want to work on bigger problems, stop spending time on small manual tasks.

    WP-CLI doesn’t just save time —
    it changes how you think about WordPress development.

    Read more: Automation-First WordPress Development

  • Automation-First WordPress Development: How I Eliminate Manual Work in 2026

    Modern WordPress development isn’t about knowing more hooks or installing more plugins.
    It’s about removing repetition, reducing risk, and building systems that scale.

    This is how I approach WordPress development today—with an automation-first mindset.


    The Problem With Manual WordPress Work

    Most WordPress developers still:

    • Click through wp-admin for routine tasks
    • Manually test updates
    • Guess performance issues
    • Fix the same problems again and again

    This works for one site.
    It completely breaks when you manage multiple sites, plugins, or products.

    Manual work doesn’t scale. Automation does.


    What “Automation-First” Really Means

    Automation-first does not mean:

    • Over-engineering
    • Complex DevOps pipelines
    • Replacing WordPress with something else

    It means:

    • Replacing clicks with commands
    • Replacing guessing with measurement
    • Replacing one-off fixes with repeatable workflows

    If a task is repeatable, it should be automated.


    Core Areas I Always Automate

    1. Site Management (No wp-admin dependency)

    I avoid wp-admin for routine tasks like:

    • Plugin activation/deactivation
    • User management
    • Search & replace
    • Cache clearing

    Why?

    • Faster
    • Scriptable
    • Safer across environments

    This alone saves hours every week.


    2. Updates & Releases

    Before any update, I rely on:

    • Pre-update checks
    • Automated backups
    • Rollback readiness

    No blind updates.
    No “hope nothing breaks” deployments.

    Automation turns updates from a risk into a routine.


    3. Performance Debugging

    Instead of:

    “Let’s try disabling some plugins”

    I follow a system:

    1. Measure first
    2. Identify the bottleneck
    3. Fix the root cause
    4. Verify improvement

    Automation helps me see the problem clearly, not guess it.


    4. Plugin & Project Structure

    Even before writing features, I automate:

    • Folder structure
    • Namespaces
    • Security baselines
    • Reusable patterns

    This ensures every project:

    • Looks familiar
    • Is easier to maintain
    • Is ready to scale later

    Where AI Fits Into This Workflow

    AI is not a replacement for automation.
    It’s a multiplier.

    I use AI to:

    • Generate boilerplate faster
    • Refactor repetitive code
    • Explain unfamiliar code paths
    • Draft documentation

    But the system still matters more than the tool.

    Automation first.
    AI on top.


    Why This Approach Is Future-Ready

    WordPress is moving toward:

    • More tooling
    • More APIs
    • More structured development

    An automation-first mindset:

    • Adapts easily to change
    • Reduces burnout
    • Makes you faster without cutting corners

    This is how you stay relevant—not by learning everything, but by working smarter.


    Final Thought

    If you’re still doing something manually every week, ask yourself:

    “Why isn’t this automated yet?”

    That question alone can transform how you work with WordPress.

  • Here’s What I’d Do Differently If I Were Starting Today

    I’ve been working with WordPress for over a decade.
    Built client sites. Shipped plugins. Made mistakes. Got better.

    If I had to start from scratch today — no network, no portfolio, no plugins — I’d do a few things very differently.

    Here’s what I wish I knew earlier.


    1. I’d Focus on Plugins Sooner

    For too long, I stayed in the “build websites for clients” lane.
    It paid the bills — but didn’t build leverage.

    If I started today, I’d get into plugin development earlier.
    Even something tiny.
    Just enough to:

    • Learn how WordPress works behind the scenes
    • Launch something public
    • Build momentum

    2. I’d Ship Small, Then Learn

    My first plugin ideas were too big.
    I wanted dashboards, options pages, custom tables — the works.

    Now I know: small plugins teach you more, faster.
    They’re easier to test, easier to support, and more likely to reach users quickly.


    3. I’d Write and Share My Process Publicly

    I kept things private for too long.

    If I started today, I’d:

    • Write short blog posts or dev logs
    • Share problems I’m solving on Twitter or my site
    • Be more visible in the WordPress community

    It’s not about building a “brand.”
    It’s about building trust — and trust brings opportunities.


    4. I’d Pick One or Two Tools — and Go Deep

    There’s always a new framework, a new build tool, a hot repo.

    But if I were starting again, I’d skip the noise and:

    • Learn PHP and JavaScript deeply
    • Understand how WordPress hooks and filters actually work
    • Master a few core plugins/tools (like ACF, WP-CLI, or Gutenberg)

    Mastery compounds over time.


    5. I’d Learn How to Support a Plugin Before Monetizing It

    You don’t need Stripe and subscriptions on Day 1.

    You need:

    • A plugin people actually want
    • A way to handle support clearly
    • A habit of improving what you ship

    If I had started with free plugins and honest support, I would’ve been much more ready for paid ones.


    Final Thought

    There’s no perfect starting point.
    But if I could rewind — I’d start smaller, ship faster, and talk more openly.

    It’s not just about being a better developer.
    It’s about building a career with a strong foundation, one small launch at a time.

  • The 3 Questions I Ask Before Starting Any Plugin

    I used to jump into plugin ideas the moment they popped into my head.

    Now?
    I pause.
    I ask 3 simple questions before I write a single line of code.

    These questions have saved me weeks of wasted effort — and helped me build plugins that actually solve problems.


    1. Would I Use This Plugin Myself?

    This is always the first check.

    If I wouldn’t install this on one of my own sites, why should anyone else?

    It doesn’t have to be something I need daily — but it has to be:

    • Useful enough to make sense
    • Simple enough to maintain
    • Clear enough that I understand its purpose without overexplaining

    If I’m forcing it, I skip it.


    2. Does This Already Exist — and Can I Do It Better or Different?

    WordPress has 60,000+ plugins on the repo.
    The odds are high that someone’s already built a version of what I’m thinking.

    But that’s not a dealbreaker.

    I ask:

    • Can I do this in a simpler, cleaner, or more focused way?
    • Can I build for a specific user or use case that existing plugins ignore?
    • Is the current solution bloated or neglected?

    Sometimes, the best plugins are better takes on ideas that already exist.


    3. Is This a Quick Hack or a Long-Term Project?

    Not every plugin needs to be a full-time product.
    Some are small helpers, and that’s okay.

    But I decide up front:

    • Is this a side experiment or a tool I’ll support long term?
    • Am I building this for fun, for learning, or for income?
    • What happens if it gets 10,000 active installs?

    If I don’t want to maintain it later, I don’t pretend I will.


    Final Thought

    I still get excited about new ideas — that hasn’t changed.
    But now I’m more intentional about where I put my energy.

    These 3 questions help me focus on plugins that matter — to me, and to the people I build for.

    And they’ve made the whole process a lot more rewarding.

  • Why I Still Build for WordPress in 2025

    There’s a lot of noise around WordPress these days.

    Some say it’s bloated.
    Some say it’s old.
    Some think no-code tools or headless stacks will take over.

    But here I am — still building WordPress plugins, still excited to ship something new.

    Here’s why.


    1. WordPress Is Still the Web’s Backbone

    WordPress powers more than 40% of the internet — and that number hasn’t dropped in any meaningful way.

    That means:

    • The ecosystem is alive.
    • People are still launching new businesses on it.
    • And there’s still room for meaningful tools that solve real problems.

    If you’re building for the web, it still makes sense to build for WordPress.


    2. It Lets Me Ship Fast

    I don’t need a 10-step build chain or a devops pipeline to launch a plugin.
    With WordPress:

    • I can build something in a few evenings.
    • Launch it on the .org repo or my site.
    • Get feedback within days.

    It’s rare to have a platform that lets you go from idea → product → user feedback that quickly.


    3. The Plugin Model Still Works

    WordPress plugins are still a great way to:

    • Solve narrow problems
    • Reach a global audience
    • Build sustainable revenue (even as a solo dev)

    And the barrier to entry is still low — especially if you focus on quality and not hype.


    4. I Understand It Deeply

    After more than a decade in this space, I know the ins and outs of plugin building — the hooks, the gotchas, the real-world use cases.

    That matters.

    Because chasing shiny frameworks or stacks just for the sake of it often leads to half-finished ideas.
    I’d rather go deeper into something I already love — and keep leveling up there.


    5. It’s Not About the Stack — It’s About the People

    I’ve met clients, collaborators, contributors, and friends through WordPress.
    I’ve been to dozens of WordCamps.
    I’ve seen the impact this platform has — on people’s careers, businesses, and lives.

    That’s hard to walk away from.


    Final Thought

    I don’t build for WordPress because it’s perfect.
    I build for it because it’s possible.

    Possible to ship fast.
    Possible to reach real users.
    Possible to grow without a team of 10 and $100k in funding.

    And that possibility still excites me — every single day.

  • My Favorite WordPress Dev Tools in 2025

    I’ve built plugins on kitchen tables, in mountain cafes, and late at night after client calls.
    Wherever I’m working from, these tools stay with me.

    Here’s a look at the tools I reach for most often in my WordPress plugin development workflow.


    1. Local by Flywheel

    For spinning up quick WordPress sites without wasting time.

    • Fast and clean local environment
    • Great for plugin testing
    • SSL and HTTPS out of the box

    2. VS Code

    Lightweight, smart, and full of plugin extensions.
    My go-to setup includes:

    • PHP Intelephense
    • Prettier
    • Code Spell Checker
    • GitLens
    • WordPress Snippet packs

    I also keep custom code snippets for hooks and filters I often use.


    3. WP-CLI

    For me, this is non-negotiable.
    From installing plugins to managing users and cleaning databases — WP-CLI is a huge time-saver.

    Example I use often:

    wp plugin install my-plugin --activate
    

    4. Query Monitor

    The best plugin to debug performance issues, database queries, hooks, and PHP errors in real time — without bloated admin panels.


    5. InstaWP

    When I need a staging environment to test plugin behavior across themes or replicate user issues — these tools help me spin up environments quickly.


    6. Poedit

    For plugin translation and making sure my .pot files are ready for global users.
    Helpful especially when preparing free + pro versions for .org.


    7. Freemius SDK

    Still my preferred framework for handling plugin licensing, subscriptions, and usage tracking — especially during launch phases.
    (In the future I may self-host it, but it works well for now.)


    8. Notion + Apple Notes

    For documenting ideas, changelogs, email templates, and future features.
    I keep it super simple — just one clean table per plugin.


    9. Pingdom Tools + Lighthouse

    Performance and Core Web Vitals testing after every major UI/plugin update.
    Simple, honest, and browser-native.


    10. My Starter Plugin Boilerplate

    I’ve created my own internal boilerplate — nothing fancy, but it saves me from rewriting:

    • Activation hooks
    • File structures
    • Class autoloaders
    • Safe enqueue functions

    Final Thought

    Tools don’t make the dev — but they do help speed up the path between idea and execution.

    These are the ones I trust, tweak, and carry with me across every plugin project.

  • Dev Tip #6: Safely Loading JavaScript and CSS in Your Plugin

    Loading scripts the wrong way can break compatibility, affect performance, or mess up other plugins.
    Here’s how I handle script loading in WordPress the right way — clean, safe, and conflict-free.


    1. Use wp_enqueue_script() and wp_enqueue_style()

    Always enqueue scripts. Never hardcode them into the page.

    add_action( 'admin_enqueue_scripts', 'my_plugin_admin_assets' );
    
    function my_plugin_admin_assets( $hook ) {
        if ( $hook !== 'toplevel_page_my-plugin' ) return;
    
        wp_enqueue_style(
            'my-plugin-style',
            plugin_dir_url( __FILE__ ) . 'assets/css/admin.css',
            [],
            '1.0.0'
        );
    
        wp_enqueue_script(
            'my-plugin-script',
            plugin_dir_url( __FILE__ ) . 'assets/js/admin.js',
            [ 'jquery' ],
            '1.0.0',
            true // load in footer
        );
    }
    

    2. Load Assets Only When Needed

    Use the $hook parameter to load scripts only on specific admin pages.

    This reduces bloat and prevents conflicts.


    3. Use plugins_url() or plugin_dir_url()

    Avoid hardcoding plugin paths. Use WordPress functions to make your URLs dynamic.

    plugin_dir_url( __FILE__ ) . 'assets/js/script.js';

    4. Add Versioning to Bust Cache

    Always version your scripts and styles, especially during development.

    wp_enqueue_style( 'my-plugin-css', $url, [], '1.1.2' );

    Use filemtime() during dev to avoid caching issues:

    wp_enqueue_style( 'my-plugin-css', $url, [], filemtime( $path ) );

    5. Respect Dependencies

    If your script depends on jQuery or WP libraries, declare them properly.

    wp_enqueue_script( 'my-js', $url, [ 'jquery', 'wp-util' ], '1.0.0', true );

    Final Thought

    Good plugins don’t just work — they play well with others.

    Loading your assets the right way keeps your plugin fast, safe, and conflict-free — especially on sites running 20+ other plugins.

  • Dev Tip #5: Creating Custom Post Types (The Right Way)

    Custom Post Types (CPTs) are one of the most powerful features in WordPress.
    But if you’ve ever seen “badly done” CPTs, you know they can cause more pain than progress.

    Here’s how I register Custom Post Types the clean and reliable way in plugins.


    1. Use init, Not plugins_loaded

    Always hook your CPT registration to init, not plugins_loaded.

    add_action( 'init', 'my_plugin_register_post_type' );
    
    function my_plugin_register_post_type() {
        register_post_type( 'book', [
            'labels' => [
                'name'          => __( 'Books', 'my-plugin' ),
                'singular_name' => __( 'Book', 'my-plugin' ),
            ],
            'public'       => true,
            'has_archive'  => true,
            'rewrite'      => [ 'slug' => 'books' ],
            'show_in_rest' => true, // Important for block editor support
            'supports'     => [ 'title', 'editor', 'thumbnail' ],
        ]);
    }

    2. Use flush_rewrite_rules() Correctly

    Don’t flush rewrite rules on every page load.
    Do it only on plugin activation.

    register_activation_hook( __FILE__, 'my_plugin_activate' );
    
    function my_plugin_activate() {
        my_plugin_register_post_type(); // Ensure it's available before flushing
        flush_rewrite_rules();
    }

    3. Don’t Forget show_in_rest

    If you’re using Gutenberg or REST API, set:

    'show_in_rest' => true

    It enables block editing, custom fields via REST, and plays nicer with modern tools.


    4. Use textdomain in Labels

    Don’t hardcode English. Use __() or _x() with your plugin’s text domain so it’s translatable.

    'name' => __( 'Books', 'my-plugin' )

    5. Don’t Abuse CPTs

    Don’t create a CPT when a taxonomy or custom fields would work better.
    For example: if it doesn’t need its own archive, slug, or UI — maybe it’s just metadata.


    Final Thought

    CPTs are easy to register — but easy to overuse too.
    Keep them clean, REST-ready, and only when needed.

    A good rule of thumb?
    “If this was a SaaS product, would this content type deserve its own section?”