<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alok Jain</title>
    <description>The latest articles on DEV Community by Alok Jain (@alokjain_dev).</description>
    <link>https://dev.to/alokjain_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3170882%2Fa307dd38-c810-49b7-8034-582342320712.jpg</url>
      <title>DEV Community: Alok Jain</title>
      <link>https://dev.to/alokjain_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alokjain_dev"/>
    <language>en</language>
    <item>
      <title>Register your first WordPress ability</title>
      <dc:creator>Alok Jain</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:59:50 +0000</pubDate>
      <link>https://dev.to/alokjain_dev/register-your-first-wordpress-ability-2l71</link>
      <guid>https://dev.to/alokjain_dev/register-your-first-wordpress-ability-2l71</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwp2qp8nf5ifdi1e60nx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwp2qp8nf5ifdi1e60nx.png" alt="WP Snippets - Register your first WordPress ability" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Until now, every plugin that wanted an AI agent (or any outside tool) to do something on a WordPress site had to invent its own REST endpoints, its own permission checks and its own way of describing what they do. WordPress 6.9 added the &lt;strong&gt;Abilities API&lt;/strong&gt; , a single registry where plugins describe what they can do in a format both code and AI agents can read.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;ability&lt;/strong&gt; is one named action: a label, a description, input and output schemas, a permission check, and the callback that does the work. This issue registers a small read-only one that counts draft posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Register a category
&lt;/h2&gt;

&lt;p&gt;Every ability belongs to a category, so register that first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_categories_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp_register_ability_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'hsr-content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'label'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hsr'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Abilities that read site content.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hsr'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Category slugs use lowercase letters, numbers and dashes only.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Register the ability
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_abilities_api_init'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp_register_ability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'hsr/count-drafts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'label'&lt;/span&gt;               &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Count drafts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hsr'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt;         &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Returns the number of draft posts on the site.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hsr'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'category'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'hsr-content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'output_schema'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'type'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'object'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'properties'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s1"&gt;'drafts'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'execute_callback'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_count_posts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'post'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'drafts'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$counts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'edit_posts'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s1"&gt;'meta'&lt;/span&gt;                &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'show_in_rest'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'annotations'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'readonly'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The array passed to &lt;code&gt;wp_register_ability()&lt;/code&gt; reads top to bottom as four groups: what the ability is called, what it returns, what it does, and who’s allowed to run it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naming it.&lt;/strong&gt; The first argument, &lt;code&gt;hsr/count-drafts&lt;/code&gt;, is the ability’s unique name. It has to follow the &lt;code&gt;namespace/ability-name&lt;/code&gt; format, and the namespace should be your plugin’s slug so it can’t clash with anyone else’s.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;label&lt;/code&gt; is the short human-readable name a UI would show. &lt;code&gt;description&lt;/code&gt; is the longer explanation, and it matters more than it looks: when an AI agent is choosing which ability to call, this is the text it reads. Write it for someone who has never seen your code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;category&lt;/code&gt; must match a slug you’ve already registered, which is why section 1 comes first. If it doesn’t match, WordPress won’t register the ability and you’ll find a “doing it wrong” notice in &lt;code&gt;debug.log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Describing the result.&lt;/strong&gt; &lt;code&gt;output_schema&lt;/code&gt; is a JSON Schema describing what the ability returns. Here it says “an object with one key, &lt;code&gt;drafts&lt;/code&gt;, which is an integer.”&lt;/p&gt;

&lt;p&gt;This isn’t just documentation. WordPress checks the callback’s return value against this schema, and if it doesn’t match, the caller gets an error instead of the result.&lt;/p&gt;

&lt;p&gt;This ability takes no input, so there’s no &lt;code&gt;input_schema&lt;/code&gt;. When an ability does need input, that key describes it in the same JSON Schema format and WordPress validates it before your callback runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doing the work.&lt;/strong&gt; &lt;code&gt;execute_callback&lt;/code&gt; is the function that actually runs. &lt;code&gt;wp_count_posts( 'post' )&lt;/code&gt; returns an object with one property per post status, and we pick out &lt;code&gt;draft&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;(int)&lt;/code&gt; cast is there because of the schema. The counts come from the database as strings (&lt;code&gt;"3"&lt;/code&gt;, not &lt;code&gt;3&lt;/code&gt;), and a string would fail the &lt;code&gt;integer&lt;/code&gt; check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checking permission.&lt;/strong&gt; &lt;code&gt;permission_callback&lt;/code&gt; runs before &lt;code&gt;execute_callback&lt;/code&gt;, every time. If it returns &lt;code&gt;false&lt;/code&gt;, the callback never runs. Here it requires &lt;code&gt;edit_posts&lt;/code&gt;, which contributors and every role above them have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The extra settings.&lt;/strong&gt; &lt;code&gt;meta&lt;/code&gt; holds options that sit outside the core definition. &lt;code&gt;show_in_rest&lt;/code&gt; makes the ability visible and runnable through the REST API, and is off by default. The &lt;code&gt;readonly&lt;/code&gt; annotation tells callers (including AI agents) that running it won’t change anything on the site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; with &lt;code&gt;show_in_rest&lt;/code&gt; on, the permission callback is the only thing between this ability and anyone who can reach your REST API.&lt;/p&gt;

&lt;p&gt;Returning &lt;code&gt;true&lt;/code&gt; from it means every visitor succeeds, logged in or not.&lt;/p&gt;

&lt;p&gt;For a draft count that’s a small leak. For an ability that updates posts or reads customer data, it’s a real hole, so always check a specific capability.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Run it
&lt;/h2&gt;

&lt;p&gt;From WP-CLI, as a user who can edit posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'print_r( wp_get_ability( "hsr/count-drafts" )-&amp;gt;execute() );'&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an array with a &lt;code&gt;drafts&lt;/code&gt; key and your actual draft count. Run it without &lt;code&gt;--user&lt;/code&gt; and the permission check fails, which is a quick way to confirm the callback is doing its job.&lt;/p&gt;

&lt;p&gt;Over REST, with an application password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-u&lt;/span&gt; admin:YOUR_APP_PASSWORD https://yoursite.test/wp-json/wp-abilities/v1/abilities/hsr/count-drafts/run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result, as JSON. That same registered ability is what the MCP Adapter hands to AI agents like Claude, which I’ll cover in a later issue.&lt;/p&gt;

&lt;p&gt;That’s all for this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; register a second read-only ability that returns the number of pending comments (&lt;code&gt;wp_count_comments()&lt;/code&gt; will get you there). Tell me what you wrote for its description, I’m curious how different everyone’s are.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://alokjain.dev/newsletter/wp-snippets/register-your-first-wordpress-ability/" rel="noopener noreferrer"&gt;Register your first WordPress ability&lt;/a&gt; first appeared on &lt;a href="https://alokjain.dev" rel="noopener noreferrer"&gt;alokjain.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
    </item>
    <item>
      <title>Stop your staging site from emailing real customers</title>
      <dc:creator>Alok Jain</dc:creator>
      <pubDate>Fri, 09 Oct 2026 10:00:28 +0000</pubDate>
      <link>https://dev.to/alokjain_dev/stop-your-staging-site-from-emailing-real-customers-3fl5</link>
      <guid>https://dev.to/alokjain_dev/stop-your-staging-site-from-emailing-real-customers-3fl5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqs8ff4uvhmqovtcw3u33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqs8ff4uvhmqovtcw3u33.png" alt="WP Snippets - Stop your staging site from emailing real customers" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you copy a live database to staging, you copy every customer email address with it. Then a cron job runs, a WooCommerce order gets resent while you’re testing, or a form plugin fires a notification, and a real person gets an email from your staging site.&lt;/p&gt;

&lt;p&gt;WordPress 5.5 added a clean way to tell a site which &lt;strong&gt;environment&lt;/strong&gt; it’s running in. Once the site knows it’s staging, you can block outgoing mail there and leave production alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Set the environment in wp-config.php
&lt;/h2&gt;

&lt;p&gt;On the staging server, add this to &lt;code&gt;wp-config.php&lt;/code&gt; above the “That’s all, stop editing!” line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'WP_ENVIRONMENT_TYPE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'staging'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The accepted values are &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;development&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt;. If the constant isn’t set, or is set to anything else, WordPress treats the site as &lt;code&gt;production&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because this lives in &lt;code&gt;wp-config.php&lt;/code&gt; and not in the database, it survives every database copy. That’s the whole reason to use it over an option or a setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Block mail outside production
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;wp-content/mu-plugins/block-staging-mail.php&lt;/code&gt; (create the &lt;code&gt;mu-plugins&lt;/code&gt; folder if it doesn’t exist):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="cd"&gt;/**
 * Plugin Name: Block Mail Outside Production
 */&lt;/span&gt;

&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'pre_wp_mail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'production'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;wp_get_environment_type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;is_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'to'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'to'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'to'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'Mail blocked on %s: "%s" to %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;wp_get_environment_type&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nv"&gt;$atts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'subject'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nv"&gt;$to&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;pre_wp_mail&lt;/strong&gt; runs before &lt;code&gt;wp_mail()&lt;/code&gt; does any work. If the filter returns anything other than &lt;code&gt;null&lt;/code&gt;, WordPress skips sending and uses that value as the result.&lt;/p&gt;

&lt;p&gt;Returning &lt;code&gt;true&lt;/code&gt; tells the calling plugin the mail went out fine. On staging that’s what you want, so contact forms and checkouts don’t show “email failed” errors while you test.&lt;/p&gt;

&lt;p&gt;I put this in a &lt;strong&gt;must-use plugin&lt;/strong&gt; rather than the theme so it keeps working if someone switches themes on staging, and nobody can deactivate it from the Plugins screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test it with WP-CLI
&lt;/h2&gt;

&lt;p&gt;Confirm the environment first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'echo wp_get_environment_type();'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send a test mail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wp &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s1"&gt;'wp_mail( "you@example.com", "Staging test", "Hello from staging" );'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing should arrive, and &lt;code&gt;debug.log&lt;/code&gt; should have a “Mail blocked on staging” line (this needs &lt;code&gt;WP_DEBUG&lt;/code&gt; and &lt;code&gt;WP_DEBUG_LOG&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; this only catches mail sent through &lt;code&gt;wp_mail()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Most plugins use it, including SMTP plugins, which hook in further down and never get reached.&lt;/p&gt;

&lt;p&gt;A plugin that talks directly to an email service’s API (some newsletter and transactional mail plugins do this) bypasses &lt;code&gt;wp_mail()&lt;/code&gt; completely. For those, check the plugin’s own settings for a test or sandbox mode.&lt;/p&gt;

&lt;p&gt;That’s all for this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; add the constant to your staging site, drop in the mu-plugin, then place a test order or submit a form. Check &lt;code&gt;debug.log&lt;/code&gt; and tell me which plugin’s email showed up first.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://alokjain.dev/newsletter/wp-snippets/stop-your-staging-site-from-emailing-real-customers/" rel="noopener noreferrer"&gt;Stop your staging site from emailing real customers&lt;/a&gt; first appeared on &lt;a href="https://alokjain.dev" rel="noopener noreferrer"&gt;alokjain.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>Add a skip link to a classic theme</title>
      <dc:creator>Alok Jain</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:00:18 +0000</pubDate>
      <link>https://dev.to/alokjain_dev/add-a-skip-link-to-a-classic-theme-1322</link>
      <guid>https://dev.to/alokjain_dev/add-a-skip-link-to-a-classic-theme-1322</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9urqreg2zp7hezl2hxsi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9urqreg2zp7hezl2hxsi.png" alt="WP Snippets - Add a skip link to a classic theme" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keyboard users press Tab to move through a page, and on most sites the first twenty or so stops are header links and menu items. A &lt;strong&gt;skip link&lt;/strong&gt; is a hidden link at the very top that appears on the first Tab press and jumps straight to the main content.&lt;/p&gt;

&lt;p&gt;Block themes get one automatically (WordPress has added it since 5.8, pointing at the template’s &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; element). Classic themes don’t, unless the theme author built one in.&lt;/p&gt;

&lt;p&gt;Before adding anything, open your site, click in the address bar, and press Tab once. If a “Skip to content” link shows up in a corner, your theme already has one and you can stop here. Themes built on Underscores usually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Print the link with wp_body_open
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_body_open'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;a class="skip-link screen-reader-text" href="#primary"&amp;gt;'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;esc_html__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Skip to content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'hsr'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/a&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wp_body_open&lt;/code&gt; fires right after the opening &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag, which is exactly where a skip link needs to be so it’s the first thing Tab reaches. The priority of &lt;code&gt;5&lt;/code&gt; puts it ahead of anything else hooked there.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;href&lt;/code&gt; has to match the &lt;code&gt;id&lt;/code&gt; of your main content wrapper. I used &lt;code&gt;#primary&lt;/code&gt; because a lot of classic themes use it, but check your theme’s &lt;code&gt;index.php&lt;/code&gt; or &lt;code&gt;page.php&lt;/code&gt; for the real one (&lt;code&gt;#content&lt;/code&gt; and &lt;code&gt;#main&lt;/code&gt; are common too).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; &lt;code&gt;wp_body_open&lt;/code&gt; only works if the theme calls it.&lt;/p&gt;

&lt;p&gt;The function was added in WordPress 5.2, and themes older than that often skip it. If your link never appears, open &lt;code&gt;header.php&lt;/code&gt; and look for &lt;code&gt;wp_body_open();&lt;/code&gt; right after the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;If it’s missing, add it there. For a third-party theme, do this in a child theme’s copy of &lt;code&gt;header.php&lt;/code&gt;, or the next theme update will wipe it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hide it until it gets focus
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.screen-reader-text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;word-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;normal&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.skip-link.screen-reader-text&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e1e1e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first rule is the standard &lt;strong&gt;screen-reader-text&lt;/strong&gt; pattern WordPress core uses in the admin. It hides the link visually but keeps it readable by screen readers, which &lt;code&gt;display: none&lt;/code&gt; would not.&lt;/p&gt;

&lt;p&gt;The second rule brings it back when it receives keyboard focus. The &lt;code&gt;z-index&lt;/code&gt; of &lt;code&gt;100000&lt;/code&gt; keeps it above the admin bar when you’re logged in (the admin bar sits at &lt;code&gt;99999&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test it with the keyboard
&lt;/h2&gt;

&lt;p&gt;Reload the page, click in the address bar, and press Tab. The link should appear at the top left.&lt;/p&gt;

&lt;p&gt;Press Enter, then Tab again. Focus should land on the first link inside your main content, not back in the header menu.&lt;/p&gt;

&lt;p&gt;If it jumps back to the header, the &lt;code&gt;id&lt;/code&gt; in the &lt;code&gt;href&lt;/code&gt; doesn’t match anything on the page. View source and search for it.&lt;/p&gt;

&lt;p&gt;That’s all for this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Tab through your homepage without the skip link first and count how many presses it takes to reach the first link in your content. Then add the snippet and count again. Tell me the before number, I’m guessing some of you will cross 30.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://alokjain.dev/newsletter/wp-snippets/add-a-skip-link-to-a-classic-theme/" rel="noopener noreferrer"&gt;Add a skip link to a classic theme&lt;/a&gt; first appeared on &lt;a href="https://alokjain.dev" rel="noopener noreferrer"&gt;alokjain.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Defer scripts with wp_enqueue_script()</title>
      <dc:creator>Alok Jain</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:00:23 +0000</pubDate>
      <link>https://dev.to/alokjain_dev/defer-scripts-with-wpenqueuescript-3j7k</link>
      <guid>https://dev.to/alokjain_dev/defer-scripts-with-wpenqueuescript-3j7k</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fykyub5yu871bys3qxk3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fykyub5yu871bys3qxk3g.png" alt="WP Snippets - Defer scripts with wp_enqueue_script()" width="799" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A lot of themes still load every script in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, so the browser has to download and run each one before it can finish rendering the page. The old workaround was a &lt;code&gt;script_loader_tag&lt;/code&gt; filter that string-replaced &lt;code&gt;&amp;lt;script&lt;/code&gt; with &lt;code&gt;&amp;lt;script defer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;WordPress 6.3 added a proper way to do this. The fifth argument of &lt;code&gt;wp_enqueue_script()&lt;/code&gt;, which used to be only &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; for footer loading, now also accepts an array with a &lt;strong&gt;loading strategy&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Defer a script you enqueue
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_enqueue_scripts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp_enqueue_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'hsr-slider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;get_stylesheet_directory_uri&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/js/slider.js'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'1.0.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'strategy'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'defer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'in_footer'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;strategy&lt;/code&gt; takes &lt;code&gt;defer&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt;. Both let the browser keep building the page while the script downloads, which a normal script tag doesn’t.&lt;/p&gt;

&lt;p&gt;The difference is when they run. A &lt;strong&gt;deferred&lt;/strong&gt; script waits until the HTML is fully parsed, and deferred scripts run in the order they were added. An &lt;strong&gt;async&lt;/strong&gt; script runs the moment it finishes downloading, so there’s no guaranteed order.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;defer&lt;/code&gt; for almost everything. &lt;code&gt;async&lt;/code&gt; only makes sense for scripts that don’t depend on anything and nothing depends on them, like some analytics tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Defer a script from a plugin
&lt;/h2&gt;

&lt;p&gt;You don’t need to edit the plugin. Find the script’s &lt;strong&gt;handle&lt;/strong&gt; in the page source (it’s the &lt;code&gt;id&lt;/code&gt; on the script tag, without the &lt;code&gt;-js&lt;/code&gt; at the end) and set the strategy after the plugin registers it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_enqueue_scripts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;wp_script_add_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'some-plugin-script'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'strategy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'defer'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wp_script_add_data()&lt;/code&gt; attaches extra data to a script that’s already registered. It takes three things: the handle, a key, and a value. When you pass &lt;code&gt;strategy&lt;/code&gt; in the enqueue array like in snippet 1, WordPress stores it this same way behind the scenes, so both snippets end up in the same place.&lt;/p&gt;

&lt;p&gt;The catch is that it only works on a registered handle. If the plugin hasn’t registered its script yet when your code runs, the function returns &lt;code&gt;false&lt;/code&gt; and nothing happens.&lt;/p&gt;

&lt;p&gt;That’s why the priority is &lt;code&gt;20&lt;/code&gt;. It makes this run after the plugin’s own enqueue, which usually sits at the default &lt;code&gt;10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’re not sure the timing is right, check the return value while testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp_enqueue_scripts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;wp_script_add_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'some-plugin-script'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'strategy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'defer'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;error_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'some-plugin-script is not registered yet'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that line shows up in &lt;code&gt;debug.log&lt;/code&gt;, the plugin registers its script later than priority &lt;code&gt;20&lt;/code&gt; (some use a higher priority or a different hook), so bump the number up and try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Check that it worked
&lt;/h2&gt;

&lt;p&gt;View source and find the script tag. If WordPress applied the strategy, you’ll see &lt;code&gt;defer data-wp-strategy="defer"&lt;/code&gt; on it.&lt;/p&gt;

&lt;p&gt;If the attribute is missing, look at what depends on that script. WordPress won’t defer a script when something depending on it still loads normally, and it falls back without any notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; leave jQuery out of this.&lt;/p&gt;

&lt;p&gt;Plenty of themes and plugins print inline code in the page that calls &lt;code&gt;jQuery(...)&lt;/code&gt; right away. If jQuery is deferred, that code runs before jQuery exists.&lt;/p&gt;

&lt;p&gt;You’ll see it as &lt;code&gt;jQuery is not defined&lt;/code&gt; in the browser console, usually along with a slider or menu that quietly stops working.&lt;/p&gt;

&lt;p&gt;That’s all for this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; pick one non-jQuery script loading in your homepage &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and defer it with snippet 2. If it didn’t get the &lt;code&gt;data-wp-strategy&lt;/code&gt; attribute, tell me which plugin’s script held it back.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://alokjain.dev/newsletter/wp-snippets/defer-scripts-with-wp_enqueue_script/" rel="noopener noreferrer"&gt;Defer scripts with wp_enqueue_script()&lt;/a&gt; first appeared on &lt;a href="https://alokjain.dev" rel="noopener noreferrer"&gt;alokjain.dev&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
