pug-php/pug-filter-stylus
Adds Stylus stylesheet support to Pug templates in PHP, so you can write Stylus inside your .pug files and have it compiled automatically. A small filter plugin for the pug-php renderer—install it, register the filter, and keep styles alongside your markup.
What this package does: it registers .pug as a view engine with Laravel's view finder, so view('emails.welcome') will happily resolve resources/views/emails/welcome.pug. Templates are compiled to cached PHP, so at runtime you get normal Laravel view behaviour with Pug syntax on top. Release 2.1.0 widens the dependency to allow pug-php 3, which means you can now run the wrapper against the current engine line (PHP 7+ only) instead of being pinned to pug-php 2.
Minimal steps:
composer require pug/laravel
php artisan vendor:publish --provider="Pug\PugServiceProvider" # config/pug.php
Auto-discovery handles the service provider on modern Laravel; on older 5.x apps register Pug\PugServiceProvider manually in config/app.php.
Then drop a file in place and render it:
//- resources/views/hello.pug
doctype html
html
body
h1 Hello #{name}
Route::get('/hello', fn () => view('hello', ['name' => 'Taylor']));
The extension is configurable (pug.extension), and both hello.pug and hello.blade.php can coexist — resolution order is what the config says, so be deliberate if you have same-named files.
Where to look first: config/pug.php. That single file controls the extension, the compiled-template cache path, and the option array passed straight through to the underlying Pug\Pug instance. Most "why is it doing that?" questions end there.
Best first use case: don't port your app shell. Pick a high-churn, low-risk corner — transactional emails, notification templates, or a static marketing/legal page. Those are self-contained, easy to eyeball, and give you a feel for the syntax tax before you commit anything load-bearing. If the team doesn't already know Pug, converting a single mailable is a good two-hour spike that either sells the approach or kills it cheaply.
Layouts replace @extends/@section:
//- layouts/app.pug
doctype html
html
head
title #{title ?? 'App'}
body
block content
//- dashboard.pug
extends layouts/app
block content
h1 Dashboard
include/mixin cover partials and reusable UI. Mixins are the closest thing to Blade components and are the main reason teams stay with Pug — argument-driven markup with no @props ceremony. If you need class-based components with slots and attribute bags, keep those in Blade; don't rebuild them in Pug.
Data passing is identical to Blade. view(), View::share(), view composers, $errors, old() — all unchanged, because the compiled output is a plain PHP view. That also means @csrf, @auth and friends are not available; use csrf_field() / auth()->check() in an expression instead.
Incremental adoption is the sane pattern. Run Pug for emails and static pages, Blade for anything Livewire/Inertia touches. Livewire components must be Blade; don't try to fight that.
Mailables and PDFs are the sweet spot:
public function build()
{
return $this->view('emails.receipt')->with(['order' => $this->order]);
}
Same for laravel-snappy / dompdf HTML sources — Pug's indentation-heavy markup is genuinely nicer for table-based email layouts than nested Blade divs.
Deployment: compiled templates live in the cache path from config/pug.php. Add php artisan view:clear to your deploy script — and be aware php artisan view:cache (Laravel 8+) does not know about .pug files, so your Pug templates get compiled lazily on first request after deploy. On a multi-server setup, either pre-warm with a request or accept the first-hit cost.
Config sketch for production:
// config/pug.php
'extension' => '.pug',
'basedir' => resource_path('views'),
'options' => [
'cache' => env('APP_ENV') === 'production',
'pretty' => false,
],
basedir matters: without it, include/extends paths resolve unpredictably once templates live in subdirectories. Set it explicitly and use paths relative to resources/views.
Extension point: custom keywords/filters go in the options array (custom_keywords, etc.) or via a small service provider that rebinds the Pug\Pug instance. This is the escape hatch for domain-specific helpers — e.g. a +money($amount) mixin is usually enough, but a real keyword is there if you need it.
Upgrading to 2.1.0 / pug-php 3. This is the headline change and it is potentially breaking for anyone with a hand-tuned options array:
prettyprint → pretty is the common one). If you set legacy keys in config/pug.php, they may now be silently ignored — which looks like "my output formatting changed for no reason."php expression language is the default; if you were relying on JS-flavoured expressions you may see new errors.composer update can pull pug-php 3 underneath you on an existing app. Pin explicitly (pug/laravel:^2.1 pug-php/pug:^3) and re-render every template in a branch rather than discovering it in production.Errors point at the wrong line. Compile failures reference the generated PHP, not your .pug source. Keep pretty => true in local so the compiled file in the cache directory is readable, then open it and diff against your template. A source-mapped error message is still a wish, not a feature.
Indentation is the syntax. Tabs vs spaces, or one stray space in an if block, produces either a compile error or — worse — silently wrong nesting that renders fine in dev and breaks when a conditional branch flips. Enforce a linter (pug-lint from npm works on the markup even though the engine is PHP) and an .editorconfig.
Escaping differs from Blade and it bites. #{ } escapes, !{ } does not. Teams habituated to {{ }} vs {!! !!} will interpolate user data unescaped at least once. Grep for !{ in review; treat it like {!! !!}.
You cannot mix Blade into a Pug file. No @if, no @include, no {{ }}. Raw PHP can be embedded with - lines (- $total = array_sum($items)), which is useful but is also how a template quietly becomes a controller.
Pug ≠ pug.js. The syntax is shared, but the expression language is PHP and JS-only constructs (loops with JS semantics, JS closures, Object.keys) won't work. Designers porting a Pug mockup from a Node project will hit this wall — worth saying out loud up front, because it's the most common source of "this package is broken" frustration.
Testing works normally. assertViewHas, assertSee, assertViewIs all behave, since it's a regular view render. Snapshot-test one or two compiled outputs after a pug-php major bump; that's the cheapest way to catch the option-renaming class of regression.
Don't rewrite the whole app. Pug is a genuine ergonomics win for markup-heavy, logic-light templates and a genuine liability everywhere else. The teams that get value from this package scope it to emails, PDFs and static pages, and leave Blade to do the application's job.
How can I help you explore Laravel packages today?