wp_add_inline_script doesn’t work in Block Themes (FSE) — Cause and Fix

You call wp_add_inline_script() inside a shortcode. It works fine on classic themes. You switch to a block theme — or your user does — and suddenly: nothing. No PHP error, no warning, just a silent failure and a X is not defined in the browser console.

This is a known inconsistency tracked in WordPress Trac #54958. Here’s what’s actually happening and how to fix it properly.

The Problem

The difference comes down to when shortcodes run relative to wp_head().

In a classic theme:

  1. wp_enqueue_scripts fires inside wp_head() → your handle gets registered
  2. Content renders → shortcode executes → wp_add_inline_script('my-handle', ...) ✅ handle already exists

In a block theme (FSE):

  1. get_the_block_template_html() renders blocks and shortcodes before wp_head()
  2. Shortcode executes → wp_add_inline_script('my-handle', ...) ❌ handle doesn’t exist yet
  3. wp_head() fires → wp_enqueue_scriptswp_register_script(...) — too late

This is intentional behavior in WordPress core. The comment in template-canvas.php says it explicitly:

“This needs to run before <head> so that blocks can add scripts and styles in wp_head().”

Why wp_add_inline_script specifically breaks

This is the subtle part. Not everything breaks — just wp_add_inline_script.

wp_enqueue_script('handle') without prior registration still works. WordPress adds the handle to a queue and has a mechanism to automatically re-enqueue a prematurely enqueued script once it gets registered — it resolves everything later when printing scripts.

wp_add_inline_script('handle', ...) is different. It immediately accesses $wp_scripts->registered['handle'] at call time. If the handle isn’t registered yet, it returns false — silently. No error, no warning.

So: enqueue tolerates a not-yet-registered handle. Inline script doesn’t. That’s the whole trap.

The Fix: register on init

Move wp_register_script to the init hook. It runs before blocks render, so the handle is ready when the shortcode needs it.

// ❌ Before — registration on wp_enqueue_scripts (too late for FSE)
add_action( 'wp_enqueue_scripts', function() {
    wp_register_script( 'my-handle', plugin_dir_url(__FILE__) . 'script.js', [], '1.0', true );
    wp_localize_script( 'my-handle', 'myData', [ 'ajax_url' => admin_url('admin-ajax.php') ] );
});

// ✅ After — registration on init (before block rendering)
add_action( 'init', function() {
    wp_register_script( 'my-handle', plugin_dir_url(__FILE__) . 'script.js', [], '1.0', true );
});

add_action( 'wp_enqueue_scripts', function() {
    // wp_localize_script works here — handle is already registered
    wp_localize_script( 'my-handle', 'myData', [ 'ajax_url' => admin_url('admin-ajax.php') ] );
});

// Shortcode — no changes needed
function my_shortcode() {
    wp_add_inline_script( 'my-handle', 'const config = ' . wp_json_encode($data) . ';', 'before' );
    wp_enqueue_script( 'my-handle' );
    return '<div>...</div>';
}

The shortcode stays untouched. Just move the registration earlier. This works for both classic and block themes — registering on init is explicitly documented as valid in the WordPress developer reference.

What doesn’t work as an alternative

enqueue_block_assets: fires inside wp_enqueue_scripts — same timing problem.

wp_is_block_theme() conditional: fragile, duplicates code, breaks if the user switches themes. It forces you to maintain two code paths for something that has a single clean solution.

Hook execution order (reference)

plugins_loaded
  └─ init                          ← register scripts here
       └─ wp_loaded
            └─ template_redirect
                 └─ [FSE] get_the_block_template_html()  ← shortcodes run here
                      └─ wp_head()
                           └─ wp_enqueue_scripts          ← localize/enqueue here
                 └─ [Classic] the_content / shortcodes    ← classic themes run here
            └─ wp_footer
                 └─ wp_print_footer_scripts

Practical Rule

If your plugin uses wp_add_inline_script() inside a shortcode: register the script on init, localize on wp_enqueue_scripts, enqueue inside the shortcode. Zero conditional logic, works everywhere.

I ran into this while working on a plugin that uses shortcodes to render dynamic content. Took longer than I’d like to admit to figure out that the fix was a one-line change in hook priority — not anything inside the shortcode itself.


References:


Enjoyed this post?

Subscribe for more content like this.