⌘K

Front-End Integration

Adding a shortcode from a plugin

render_content_html() has no filter hook a plugin can register a new shortcode into directly — extending it would mean editing a core file. A theme can do this cleanly via the render_content_html filter point (see Filters), but a plugin, being independent of whichever theme is active, instead wraps the entire page response in an output buffer and does a find-and-replace on its own marker:

$isCli   = PHP_SAPI === 'cli';
$isAdmin = strpos($_SERVER['REQUEST_URI'] ?? '', '/admin/') !== false;

if (!$isCli && !$isAdmin) {
    ob_start(function (string $html): string {
        if (strpos($html, '[my_shortcode]') === false) {
            return $html; // nothing to do — page passes through unmodified
        }
        $html = str_replace('[my_shortcode]', my_plugin_render_widget_html(), $html);

        // Inject CSS/JS only on pages that actually use the shortcode.
        $assets = my_plugin_render_assets();
        return stripos($html, '</head>') !== false
            ? preg_replace('/<\/head>/i', $assets . '</head>', $html, 1)
            : $assets . $html;
    });
}

A few things worth knowing before building on this:

  • Never nest a second output buffer inside the callback. If a function called from inside the display handler tries ob_start()/ob_get_clean() itself, PHP throws a fatal error ("Cannot use output buffering in output buffering display handlers"). Build widget HTML with plain string concatenation or heredoc instead.
  • Guard against admin and CLI contexts — the buffer should only ever wrap front-end HTML responses.
  • Check for your marker before doing any work. strpos($html, '[my_shortcode]') === false is a cheap early-out — most page loads on a real site won't contain your shortcode, and every one of those should pay effectively zero cost for your plugin being active.

Public-facing endpoints

Any PHP file inside your plugin's folder is reachable directly by URL (/plugins/my-plugin/my-plugin.php). Use this for AJAX endpoints, form submission handlers, or anything else front-end JavaScript needs to talk to. These bypass index.php's routing entirely — self-contained scripts that load only what they need:

<?php
require_once __DIR__ . '/my-plugin-functions.php';

header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store');

$action = $_GET['action'] ?? '';
// ... handle $action, echo json_encode(...), exit;

Protect files that should never be requested directly (settings loaders, security helpers) with a .htaccess in your plugin's root:

<FilesMatch "\.(json)$">
    <IfModule mod_authz_core.c>
        Require all denied
    </IfModule>
    <IfModule !mod_authz_core.c>
        Deny from all
    </IfModule>
</FilesMatch>