vedmant/laravel-shortcodes
Add WordPress-style shortcodes to your Laravel app. Register custom tags with attributes, parse them in Blade views or any content, and render dynamic output cleanly. Lightweight and easy to extend, ideal for CMS-like content and reusable snippets.
Because this package is small and the repository link is not available in the metadata, verify class names and method signatures against vendor/vedmant/laravel-shortcodes/src before copying snippets. The concepts below are the day-to-day patterns you will use regardless of exact naming.
Install and confirm discovery
composer require vedmant/laravel-shortcodes
Laravel should auto-discover the service provider. If the package ships a config file, publish it and inspect the defaults:
php artisan vendor:publish --provider="Vedmant\LaravelShortcodes\ShortcodesServiceProvider"
If that provider class does not exist, find the real one in vendor/vedmant/laravel-shortcodes/composer.json under extra.laravel.providers.
Where to look first
vendor/vedmant/laravel-shortcodes/README.md — registration API and callback signature.vendor/vedmant/laravel-shortcodes/src/ — the parser and facade. Confirm whether the callback receives ($attributes, $content) or ($attributes, $content, $tag).App\Providers\AppServiceProvider::boot() — the simplest place to register your first shortcode.First use case
The natural use case is editor-authored content stored in the database: CMS pages, blog bodies, product descriptions, email templates. Editors write WordPress-style tags and your application renders them safely:
{!! Shortcode::compile($page->body) !!}
Start with one shortcode and one rendering path, confirm the output in php artisan tinker, then expand.
use Vedmant\LaravelShortcodes\Facades\Shortcode;
public function boot(): void
{
Shortcode::register('button', function (array $attrs, ?string $content = null) {
$url = e($attrs['url'] ?? '#');
$class = e($attrs['class'] ?? 'btn btn-primary');
$label = e($content ?: ($attrs['label'] ?? 'Read more'));
return "<a href=\"{$url}\" class=\"{$class}\">{$label}</a>";
});
}
{!! Shortcode::compile('[button url="https://example.com" class="btn btn-lg"]Get started[/button]') !!}
Use a dedicated service provider
Do not scatter Shortcode::register() calls across controllers. Create App\Providers\ShortcodeServiceProvider and register every shortcode in one place. This becomes your catalogue: it is easy to audit, document, and test.
class ShortcodeServiceProvider extends ServiceProvider
{
public function boot(): void
{
Shortcode::register('button', [ButtonShortcode::class, 'render']);
Shortcode::register('alert', [AlertShortcode::class, 'render']);
Shortcode::register('embed', [EmbedShortcode::class, 'render']);
}
}
Render shortcodes through Blade views
Inline HTML strings become unmaintainable quickly. Delegate to a view and keep the callback thin:
Shortcode::register('alert', function (array $attrs, ?string $content = null) {
return view('shortcodes.alert', [
'type' => in_array($attrs['type'] ?? 'info', ['info', 'warning', 'danger'], true)
? $attrs['type']
: 'info',
'slot' => $content ?? '',
])->render();
});
{{-- resources/views/shortcodes/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
{!! $slot !!}
</div>
This gives you Blade escaping, component reuse, and normal view caching for free.
Normalize and validate attributes in one place
Attributes arrive as raw editor input. Treat them like request input:
private function attributes(array $attrs): array
{
return [
'url' => filter_var($attrs['url'] ?? '', FILTER_VALIDATE_URL) ?: '#',
'target' => ($attrs['target'] ?? '_self') === '_blank' ? '_blank' : '_self',
'class' => preg_replace('/[^a-zA-Z0-9 _-]/', '', $attrs['class'] ?? ''),
];
}
Do not rely on the parser to validate anything. It parses; your callback authorizes and escapes.
Distinguish self-closing and enclosed shortcodes
WordPress-style syntax supports both:
[embed src="https://youtube.com/watch?v=abc"]
[button url="/pricing"]See pricing[/button]
Decide explicitly whether $content is null, '', or the raw inner string for self-closing tags, and test both forms. If your callback assumes content exists, self-closing usage will produce empty output or errors.
Add a model accessor or presenter
Keep raw content in the database and render on output. A model accessor is convenient for read-only display:
public function getRenderedBodyAttribute(): string
{
return Shortcode::compile($this->body);
}
Do not persist the compiled result back into the same column. Keep raw content editable and compiled content derived.
Expose a Blade directive or helper
If shortcode rendering is common in views, hide the facade behind one entry point:
Blade::directive('shortcodes', function ($expression) {
return "<?php echo app('shortcode')->compile($expression); ?>";
});
@shortcodes($page->body)
This makes it trivial to swap the implementation later without touching every view.
Cache compiled content
Shortcode compilation is regex and string work on every request. If content changes rarely, cache the rendered output keyed by a content hash or model version:
public function getRenderedBodyAttribute(): string
{
return Cache::remember(
"page:{$this->id}:body:".md5($this->body),
now()->addDay(),
fn () => Shortcode::compile($this->body)
);
}
Invalidate on save, or include the content hash in the key as above.
Document the supported tags for editors
Maintain a short reference — tag name, attributes, whether it is self-closing, and an example. Editors will paste shortcodes into a WYSIWYG field; if the catalogue is not documented, you will get malformed tags and support requests. Consider restricting the editor’s shortcode palette to a known list.
Test the rendering path directly
Shortcode rendering is pure enough to test without HTTP:
public function test_button_shortcode_renders_escaped_attributes(): void
{
$html = Shortcode::compile('[button url="/x" class="a" onclick="alert(1)"]Go[/button]');
$this->assertStringContainsString('href="/x"', $html);
$this->assertStringNotContainsString('onclick=', $html);
}
Add cases for unknown tags, missing attributes, self-closing tags, nested tags, and encoded quotes.
Shortcode output is a trust boundary. Shortcode::compile() returns HTML that you will typically echo with {!! !!}. Every callback must escape attribute values and decide deliberately whether enclosed content is trusted HTML or untrusted text. If editors are not fully trusted, sanitize the compiled result with a library such as HTML Purifier, or escape content inside callbacks.
Compile once. Running compile() on already-compiled output can re-interpret generated HTML that happens to contain square brackets, double-render nested structures, or corrupt attributes. Store raw content, compile at the edge, and avoid chaining compile calls in accessors, Blade, and controllers for the same value.
Unknown shortcodes are a product decision. Confirm how the package handles unregistered tags: strip them, leave them verbatim, or throw. Leaving them verbatim is often best for authoring because editors see their mistake; stripping is better for public output. Test [unknown]text[/unknown] explicitly and make the behavior intentional.
Attribute parsing has edge cases. WordPress-style syntax is not HTML or JSON. Quotes inside values, boolean attributes ([video autoplay]), unquoted values, and multiline attributes often behave differently than expected. Prefer quoted attributes in documentation and test each attribute type you expose.
Nested shortcodes are the usual failure point. Regex-based parsers frequently fail on same-name nesting such as [box][box]inner[/box][/box]. Verify whether nested compilation is recursive. If it is not, either flatten your markup or compile inner content in the callback before wrapping it.
Large or malformed content can be expensive. Regex parsing over multi-megabyte CMS bodies, especially unterminated tags, can be slow. Cap body length, cache compiled output, and avoid compiling inside loops over collections.
Laravel version support is a real risk. The last release is January 2023, which predates Laravel 11. Check composer.json constraints and test auto-discovery, facade aliases, and config caching on your target Laravel version. If constraints block installation, use --ignore-platform-reqs only as a temporary diagnostic, not a production strategy.
Small package means you own the risk. With 27 stars and no recent release, audit the parser source before adopting it. Wrap it behind your own ShortcodeRenderer service so you can replace it with thunderer/shortcode or a custom parser without changing views, models, or tests.
Registration order and conflicts. If two providers register the same tag, the later registration may silently overwrite the earlier one. Keep the catalogue in one provider and add a test that asserts every expected tag is registered.
Config caching. If the package merges a config file, run php artisan config:cache in deployment and confirm that registration does not depend on runtime-only values. Shortcodes registered conditionally from request data will behave differently under config caching.
Debugging workflow. Isolate failures in Tinker before touching Blade:
dump(Shortcode::compile('[button url="/x"]Go[/button]'));
If output is empty, check that the service provider booted, the tag name matches exactly (case sensitivity varies), and the callback signature matches what the parser passes. If output is partially rendered, the parser is likely splitting on an attribute value containing a bracket or quote.
Extension points to look for. Inspect the source for a parser instance bound in the container, a config-driven tag map, or stripper/compiler classes. If the package exposes the underlying parser, prefer extending it over reimplementing parsing in your callbacks. If it does not, keep your callbacks small and put all custom logic in your own classes.
How can I help you explore Laravel packages today?