proai/laravel-handlebars
Laravel Handlebars integrates the Handlebars templating engine into Laravel, letting you render .hbs views through Laravel's view system with helpers, partials, layouts, and caching. Use it alongside Blade for clean, logic-light templates in Laravel apps.
Install the package and let Laravel auto-discover the service provider:
composer require proai/laravel-handlebars
php artisan vendor:publish --tag=handlebars-config
Open config/handlebars.php first — it is the whole package in one file. You care about four keys:
resources/views, with a .hbs / .handlebars extension),storage/framework/... — must be writable),helpers and partials maps.Drop a template in and render it:
// resources/views/emails/welcome.hbs
// <h1>Hello {{user.name}}</h1>
// {{#each items}}<li>{{this}}</li>{{/each}}
use ProAI\Handlebars\Facades\Handlebars;
return Handlebars::render('emails.welcome', [
'user' => ['name' => 'Ada'],
'items' => ['A', 'B'],
]);
When something doesn't behave, the source of truth is not this package — it's LightnCandy. Every flag, helper signature, and escaping rule comes from there. Read the LightnCandy README once and this wrapper becomes obvious.
Best first use case: transactional email or any HTML you also want to render client-side. That's the entire reason to accept the extra moving part.
Blade-free server rendering. Use it where you want the same .hbs file rendered by PHP on first paint and by Handlebars.js in the browser afterwards. Keep the template free of Blade syntax — no @if, no {{ $var }}; Handlebars only understands {{var}}, {{#if}}, {{#each}}, {{#with}}.
Layouts via partials. Register partials in config (or at runtime) and compose:
// config/handlebars.php
'partials' => [
'layout/header' => resource_path('views/partials/header.hbs'),
'layout/footer' => resource_path('views/partials/footer.hbs'),
],
{{> layout/header}}
<main>{{{body}}}</main>
{{> layout/footer}}
Custom helpers for domain logic. Keep them small, pure, and in a dedicated service provider so they're testable independently of the template:
Handlebars::helper('money', fn ($amount) => number_format($amount / 100, 2) . ' €');
{{money order.total}}
Precompile on deploy, not on request. Compilation is expensive (it parses to an AST and emits PHP). In production, warm the cache during your deploy script — render each template once, or point the compile path at a directory you ship — and let the request path only include the compiled file. Turn on OPcache and this becomes effectively free.
Controller/view integration. If the package registers a view engine for the configured extension, return view('emails.welcome', $data); resolves to your .hbs file and you get Laravel's normal view pipeline (data binding, view:cache semantics). If you'd rather not rely on engine registration, wrap the facade in a single Renderer class in your app — one seam to mock in tests and one place to swap the backend later.
Eloquent data. Handlebars is dumb about objects; convert first.
$data = $order->load('items')->toArray(); // or ->toJson() then json_decode(..., true)
Models, Collection instances, and Carbon objects will not behave the way {{item.created_at}} expects. toArray() gives you plain nested arrays and predictable {{#each}} behaviour.
Testing. Assert on the rendered string, not on the compiled output:
$this->assertStringContainsString('Hello Ada', Handlebars::render('emails.welcome', $data));
Add a smoke test per template so a broken partial fails CI, not production.
FLAG_PROPERTY, FLAG_METHOD). Treat templates as source code under version control, not as data..hbs file and seeing the old output means the compiled artifact wasn't invalidated. php artisan view:clear only knows about Blade; make sure your deploy or local workflow clears the handlebars compile directory too, or enable runtime recompilation in local only.{{#each}} over an object misbehaves, or {{this}} renders [object Object], or a helper's options.fn() returns nothing, you're usually missing a LightnCandy flag rather than hitting a package bug. Start with FLAG_ERROR_EXCEPTION so compile failures throw instead of silently rendering empty strings.{{ }} vs {{{ }}}. Double braces HTML-escape (&, <, >, quotes); triple braces are raw. The most common XSS in Handlebars apps is a triple-stache around user input. If you need no escaping globally, that's FLAG_NOESCAPE — use it only for non-HTML output like plain-text emails or config generation.($args..., $options) where $options carries fn, inverse, hash, data. Returning a string from a block helper is different from echoing it. Getting $options['fn']($this) vs $options['inverse']($this) backwards produces silently blank blocks.@include, @csrf, {{ $var }} all silently do nothing or render literally. If you need CSRF tokens in a form, pass them in as data.{{#each}} and nested lookups. Objects work partially and inconsistently; convert at the boundary.composer.json, check the Laravel constraint against your version before upgrading Laravel, and keep the abstraction thin — a single Renderer wrapper class means you can replace this with a direct LightnCandy call (or a different engine) in an afternoon.How can I help you explore Laravel packages today?