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]') === falseis 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>
