murdercode/laravel-shortcode-plus
A Laravel package for parsing shortcodes inside content, with support for attributes and custom handlers. Register your own shortcodes, then render them in Blade views or user-generated text to keep templates clean and content flexible without hardcoding markup.
Install and register the service provider
composer require murdercode/laravel-shortcode-plus
Laravel's package auto-discovery registers the service provider and the Shortcode facade. Publish the config file first — this is the registry you will edit most often:
php artisan vendor:publish --tag=shortcode-config
That gives you config/shortcode.php, which contains a shortcodes map of name → class and a strip_unknown flag. Check the published file rather than the README for the exact key names in the 5.x line; they have drifted between majors.
Write your first shortcode
Add an entry to the config map, then create a class that extends the package's abstract shortcode. The two methods you must implement are the name registration and the transform:
namespace App\Shortcodes;
use Murdercode\Shortcode\Shortcode;
class Callout extends Shortcode
{
public function register(string $shortcode): string
{
return 'callout';
}
public function process($content, $shortcode): string
{
return view('shortcodes.callout', ['slot' => $content])->render();
}
}
// config/shortcode.php
'shortcodes' => [
'callout' => \App\Shortcodes\Callout::class,
],
First use case
The canonical fit is CMS-style body copy: editorial content, marketing pages, or any HTML stored in the database where a non-developer needs to drop a component into prose without touching Blade. Compile in a Blade view:
{!! Shortcode::compile($page->body) !!}
or, if the package's Blade directive is available in your version, @shortcode($page->body). Verify the abstract class signature against vendor/murdercode/laravel-shortcode-plus/src/Shortcode.php before writing anything non-trivial — that file is the real documentation, and the method names/signatures are what break between majors.
Class-per-shortcode, config-driven registry. Resist the temptation to put logic in the config array. The registry should be a pure map; every transform lives in its own class that can be unit tested without booting the shortcode parser. This is also what makes the package's strip_unknown behaviour tolerable — you can see exactly what is and isn't registered.
Store raw, compile at render. The single biggest architectural decision. Storing compiled HTML means re-rendering every page when a shortcode's markup changes; storing raw source means you pay the parse cost on every request. Take the second option and cache the compiled output keyed on the raw body plus an updated_at timestamp.
Wrap the facade behind your own service. Shortcode::compile() is a static-ish call into a container-bound resolver, which makes it awkward to swap in tests and impossible to add instrumentation. A thin ShortcodeRenderer wrapper gives you a single place to add caching, logging of unknown/stripped tags, and an escape hatch if the package is abandoned.
Enclosing vs self-closing. Use enclosing shortcodes ([callout]…[/callout]) for content-wrapping components where the author provides the inner text, and self-closing for parameter-driven embeds. Keep the $content handling in process() defensive — it can be empty, and it is user-supplied.
Attributes and validation. Shortcodes are a user-facing input surface. Anything parsed out of the tag (attributes, IDs, slugs) is untrusted: validate against a whitelist and fail closed. For a [video id="..."] shortcode, resolve the ID against known records rather than interpolating it into a URL.
Integration points beyond page bodies. The same compile step is useful for:
Testing. Unit test each shortcode class directly — new Callout()->process('hi', 'callout') — and use a single feature test asserting the facade's end-to-end output for one representative document. That keeps the bulk of the suite decoupled from the package's parser internals.
Output is raw HTML. Whatever process() returns goes to the browser unescaped. Escape the $content you interpolate, and never pass user attributes through without sanitising. This is the failure mode that turns a CMS feature into a stored-XSS vector.
Config caching. After editing config/shortcode.php in a deployed environment, php artisan config:cache will serve the stale registry and new shortcodes will silently render as plain text (or vanish, depending on strip_unknown). Add config-cache rebuild to the deploy script, not to muscle memory.
strip_unknown semantics. With stripping on, an unregistered [foo] disappears entirely — which hides typos from authors and makes the failure look like data loss. With it off, the literal brackets leak into the rendered page. Either way, log unknown tags during compile (or scan content in a scheduled job) so editorial mistakes surface as a report rather than a mystery.
Whitespace and case sensitivity. Shortcode names are matched literally. [Callout], [callout ], and [callout\n] may not all resolve. Write a lint command that scans stored content for bracket patterns and reports near-misses; it pays for itself the first week.
Nesting order. Nested shortcodes are processed in a defined order that is easy to get wrong when you introduce a shortcode that emits another shortcode's syntax. If a shortcode's output legitimately contains bracket syntax, escape it or you'll get double-processing.
Performance. Compilation is regex work over the whole body, so it scales with content length, not with the number of shortcodes. For a listing page rendering 50 articles, compile once per article and cache; don't compile inside a loop that also re-renders on every request.
Debugging. When a shortcode doesn't fire, check in this order: is it in the config map, is config cached, does register() return exactly the string used in the content, and does the class resolve from the container (constructor dependencies are a common break). Dumping Shortcode::compile('[callout]x[/callout]') in tinker isolates parser problems from view problems in one step.
Extension points. Overriding the published config, subclassing the parser or the abstract shortcode, and the Blade directive are the seams available. Prefer subclassing your own shortcode classes over extending the package internals — the parser's protected methods are the least stable surface in a 5.x line.
On v5.3.1. The release notes carry only a compare link, so there is no documented behaviour change. It's a patch on the 5.3 branch: no migration expected, no config or signature changes implied. Pin to ^5.3.1 and read the diff on GitHub if you were tracking a specific bug. Treat any future major as a full re-read of the abstract Shortcode class, since that's where breakage lands.
How can I help you explore Laravel packages today?