surgiie/blade
Surgiie Blade is a Laravel package that extends Blade with reusable UI components and helpers, making it easier to build clean, consistent interfaces faster with less repetitive markup. It streamlines common frontend patterns so you can focus on application logic instead of rewriting template code.
The pitch: a standalone wrapper around Laravel's Blade compiler so {{ $var }}, @if, @foreach, and directives work on any text — emails, invoices, CLI output, markdown, config files, SQL snippets — not just .blade.php files served by a Laravel app.
First steps:
composer require surgiie/blade (verify the exact constraint in composer.json; this package is typically used alongside or instead of the full framework).src/ — a package this small is usually 1–3 classes (Blade, a compiler/engine wrapper, maybe a directive registrar). The class signature tells you everything: view paths array, cache path, and methods like render(), make(), directive(), renderString()/compileString().Minimal usage:
use Surgiie\Blade\Blade;
$blade = new Blade(
__DIR__.'/templates', // where your .blade.php files live
__DIR__.'/cache' // where compiled PHP is written
);
echo $blade->render('emails.welcome', [
'user' => $user,
'product' => $product,
]);
First real use case: transactional emails. Drop resources/templates/emails/welcome.blade.php into the templates dir, render with a data array, feed the string into your mailer. You get layouts, partials, and conditionals without pulling in the full view() stack.
Where to look second: the tests directory. For small compiler wrappers, tests are the actual documentation — they show string rendering, custom directives, and edge cases (nested includes, raw vs. escaped output) faster than the README.
Render strings, not just files. The main reason to reach for this over jenssegers/blade is templating arbitrary text you don't want on disk — DB-stored notification templates, admin-editable copy, generated reports:
$output = $blade->renderString(
'Dear {{ $name }}, your invoice #{{ $number }} is due {{ $due }}.',
['name' => 'Ada', 'number' => 1042, 'due' => '2024-06-01']
);
(Confirm the method name — renderString, compileString, or similar — against the README; the concept is the same across wrappers.)
Treat it as a rendering layer, not a framework. Compose it into services rather than calling render() everywhere:
final class InvoiceRenderer
{
public function __construct(private Blade $blade) {}
public function render(Invoice $invoice): string
{
return $this->blade->render('invoices.pdf', [
'invoice' => $invoice,
'lines' => $invoice->lines,
'total' => $invoice->formattedTotal(),
]);
}
}
Bind the Blade instance as a singleton in your container so custom directives are registered once.
Custom directives are the extension point for domain templates. If templates are user-editable (email editor, CMS), register a small, audited set of directives instead of letting users write PHP:
$blade->directive('currency', fn ($expr) => "<?php echo number_format({$expr}, 2); ?>");
$blade->directive('tenant', fn () => "<?php echo e(config('app.tenant')); ?>");
Keep the count small. Every directive is a compile-time code injection surface.
Use partials for shared fragments. @include('partials.footer') and @each('partials.line', $lines, 'line') work as expected, so invoice line items, email footers, and repeated CLI sections stay DRY. Resolve includes relative to the view paths you passed in — verify the package doesn't require an absolute prefix on nested includes.
In a Laravel app, use it outside the view layer. Don't replace view() in controllers. Use it where Laravel's view system is awkward: generating files on disk, rendering templates that live in a package's resources/, or compiling templates loaded from a database. In non-Laravel apps (a CLI tool, a Symfony service), this package is arguably the cleanest way to get Blade syntax without illuminate/view's full bootstrapping ceremony.
Test rendered output, not internals. Snapshot or assert on the string:
$this->assertStringContainsString('Invoice #1042', $renderer->render($invoice));
Because output is a plain string, golden-file tests are cheap and catch whitespace regressions in emails.
The cache directory is not optional and must be writable. Most standalone Blade wrappers write compiled PHP to disk and include it. If the directory is missing or permissions are wrong, you get a confusing include/require error far from the real cause. Create it at bootstrap, not lazily.
Compiled templates are cached by mtime. During development this is usually fine, but in deploy scenarios where mtimes can be preserved (rsync without --checksum, Docker layer caching), stale compiled output survives. Clear the cache directory as part of your deploy or composer post-autoload-dump script.
HTML escaping is the default and will corrupt non-HTML output. {{ $value }} runs e(), which escapes &, <, >, ", ' for HTML. If you're rendering JSON, YAML, SQL, shell scripts, or plain text emails, this silently mangles values. Use {!! !!} for raw output or register a custom escaper. For JSON specifically, prefer @json($data) or json_encode() in the template over hand-interpolating. This is the #1 source of "why does my rendered YAML have &" bugs.
Don't pass user-controlled template names. $blade->render($request->input('template')) is a directory traversal waiting to happen. Resolve template names against a whitelist or a fixed enum.
User-editable templates are an RCE surface. Blade compiles to PHP. If end users can edit templates, they can write @php blocks and call arbitrary functions unless you strip or sandbox them. For untrusted templates, either disable @php / @eval / @include via a custom compiler, or reach for a logic-less engine instead.
Directive registration order matters. Register custom directives before the first render. Some wrappers compile lazily and cache the compiler, so a directive added after rendering may not take effect until the instance is rebuilt.
@ collisions. Email addresses in templates ([email protected]) are generally fine, but @if, @foreach etc. appearing in prose, markdown, or code samples will be parsed as directives. Escape with @@if (Blade's standard escape) when outputting literal directive syntax.
Differences from Laravel's view() will bite. There's no View::share(), no view composers, no @inject without container plumbing, no old()/csrf_field()/auth() defaults, and __()/translation helpers may not exist in a non-Laravel context. Audit templates migrated from a Laravel app for these.
Debugging: read the compiled file. When a template behaves oddly, find its compiled artifact in the cache directory and read the generated PHP. It's the fastest way to see whether a directive expanded the way you expected or whether your custom directive produced malformed PHP.
Whitespace is meaningful in text output. Blade's {{ }} does not trim surrounding whitespace the way you might hope; in indentation-sensitive contexts (YAML, Python snippets, markdown), use {{- -}}-style trimming if supported, or structure templates so directives sit on their own lines.
Watch for accidental recompilation in hot paths. Rendering thousands of strings in a loop (renderString) recompiles each time unless the package hashes and caches by template content. If you're rendering large volumes, cache the compiled output yourself or render from files, which the mtime cache handles.
Extension points to lean on: custom directives, a subclass that overrides the compiler or output escaper, and (if exposed) the underlying Illuminate\View\Compilers\BladeCompiler. If you need a directive that only runs in non-HTML contexts, an escaper override beats sprinkling {!! !!} across every template.
How can I help you explore Laravel packages today?