pug-php/pug
Pug-php/pug brings the Pug (formerly Jade) template engine to PHP. Write clean, indentation-based templates that compile to HTML, with Laravel integration, caching, and a concise syntax for faster, more readable views.
1. Install the package. There is no official Laravel service provider bundled, so you wire it up yourself:
composer require pug-php/pug
2. Install the underlying Node compiler. pug-php/pug shells out to the real pug package via Node, so the runtime host needs both:
node -v # must exist
npm install -g pug # or a local node_modules/pug in the project
Verify with pug --version. If pug is not on PATH, point the package at it later via the command option.
3. Add a config file at config/pug.php:
<?php
use Pug\Pug;
return [
'basedir' => resource_path('views'),
'paths' => [resource_path('views')],
'extension' => '.pug',
'cache' => env('APP_DEBUG') ? false : storage_path('framework/views/pug'),
'cache_dir' => storage_path('framework/views/pug'),
'pretty' => env('APP_DEBUG'),
'strict' => env('APP_DEBUG'),
'expressionLanguage' => 'js', // 'php' if you want PHP expressions in templates
];
4. Write a tiny view engine. Pug\Pug renders directly, so extending Engine (not CompilerEngine) is the simplest correct integration:
<?php
namespace App\View;
use Illuminate\View\Engines\Engine;
use Pug\Pug;
class PugEngine extends Engine
{
public function __construct(protected Pug $pug) {}
public function get($path, array $data = [])
{
return $this->pug->render($path, $data);
}
}
5. Register it. In AppServiceProvider::boot():
use App\View\PugEngine;
use Pug\Pug;
$this->app->singleton(Pug::class, fn () => new Pug(config('pug')));
$this->app['view.engine.resolver']->register('pug', function () {
return new PugEngine($this->app->make(Pug::class));
});
$this->app['view']->addExtension('pug', 'pug');
6. First template. resources/views/welcome.pug:
doctype html
html
head
title= title
body
h1 Hello #{name}
ul
each item in items
li= item
Route it: Route::view('/welcome', 'welcome', ['title' => 'Hi', 'name' => 'World', 'items' => ['a','b']]);
That's the whole loop. You can now use .pug files anywhere Laravel accepts a view name — view(), Mailable::view(), View::composer(), error pages, etc.
First meaningful use case: a marketing landing page or transactional email where Blade's verbosity gets in the way — Pug's indentation syntax pays off immediately on markup-heavy files.
Layouts and partials. Use Pug's native extends/block/include — do not try to mix Blade directives:
//- resources/views/layouts/app.pug
doctype html
html
head
title= title
body
block content
//- resources/views/pages/about.pug
extends ../layouts/app
block content
h1 About
include ../partials/footer
Set basedir to resources/views so absolute-style include /partials/x resolves.
Mixins as reusable components — the closest thing to Blade components:
mixin button(label, href)
a.btn(href=href)&attributes(attributes)= label
+button('Save', route).btn-primary
Passing Laravel-generated strings (route URLs, CSRF tokens, asset paths) as variables works fine under the default JS expression language.
PHP expression language, for helper-heavy views. Set 'expressionLanguage' => 'php' and write PHP directly inside Pug expressions:
p= e($user->name)
a(href=route('profile', ['id' => $user->id])) Profile
This is the switch that makes Pug feel native to Laravel — route(), asset(), csrf_token(), auth()->user() all become usable. Keep it consistent: don't mix JS and PHP expression languages across templates in one app, or your team will get confused about which = means what.
Shared data. View::share() and view composers work transparently because Laravel merges data before handing it to PugEngine::get(). Put all data shaping in the controller or composer, not in the template.
Cache warming on deploy. Cold renders shell out to Node (~50–200 ms per file). Warm renders hit the compiled PHP cache and are fast. Add an artisan command:
foreach (File::allFiles(resource_path('views')) as $file) {
if ($file->getExtension() === 'pug') {
$pug->render($file->getRealPath(), []); // just to compile
}
}
Run it in your deploy pipeline after view:cache. Note that php artisan view:clear does not clear Pug's cache dir — delete storage/framework/views/pug explicitly.
Inline rendering for tests or dynamic mail bodies:
$html = app(Pug::class)->render('p= $name', ['name' => 'Ada']);
Non-existent paths are treated as template source, which makes unit-testing templates trivial.
Filters for custom transforms:
'filters' => [
'markdown' => fn (string $text) => Str::markdown($text),
],
:markdown
# Heading
php:fpm images have no node. Add a Node layer to your Dockerfile or pick phug/phug (the pure-PHP reimplementation) if you cannot guarantee Node. This is a hard architectural decision — make it early.pug npm version. A pug@4 release can silently break generated PHP syntax with a pug-php/pug that expects pug@3. Lock it in package.json and in your image build.strict defaults to false. Undefined variables render as empty strings — you will spend an afternoon hunting a "missing" value that was never passed. Enable strict in dev.basedir is the include resolver. Relative include ../x works without it; absolute include /partials/x and extends need it set to resources/views. Get this wrong and errors are misleading ("file not found" pointing at the wrong path).rsync -a, some CI caches) can serve stale HTML. touch the .pug files or wipe the cache dir on every release.pretty => true in production adds whitespace that can break pre, textarea, and inline elements. Keep it dev-only.render(). A path that doesn't exist is compiled as source. If a view filename is typo'd, you get a confusing "unexpected token" compile error rather than "view not found" — validate paths in your engine if that bites.$pug->compile($file) returns the generated PHP source — dump it whenever a template behaves unexpectedly. This is the single most useful debugging move.command option for path discovery. When pug isn't on the shell PATH (common in containers), set 'command' => base_path('node_modules/.bin/pug'). npx pug also works but adds latency.@csrf, @auth, @foreach in a .pug file. They are meaningless. Use Pug equivalents or precompute values in the controller.= and #{} escape; != and !{} do not. Under PHP expression language, calling e($x) inline is also valid but redundant.keywords, customKeywords, plugins options) are the extension point for domain-specific shorthand — but they compile at the Node layer, so they're more work than filters. Reach for filters and mixins first.laravel-pug and similar packages are older integrations built around this engine or Phug. If you want batteries-included (publishable config, Blade-like directives, @pug inline), evaluate phug/laravel before rolling your own engine — but it swaps the engine implementation, so audit template compatibility.How can I help you explore Laravel packages today?