magentron/laravel-blade-lint
Laravel Blade Lint validates Blade templates and reports syntax errors, malformed directives, and other template issues before they reach production. Use its Artisan command locally or in CI to keep views clean and catch mistakes early.
Install as a dev dependency. Blade linting is a build-time quality gate, not a runtime feature:
composer require --dev magentron/laravel-blade-lint
The package ships a service provider, so Laravel's package discovery registers the command automatically. Confirm it landed before doing anything else:
php artisan list | grep -i blade
php artisan blade-lint --help
First run on an existing app. Execute it from the project root against your default views directory (resources/views) and treat the first output as a triage list, not a blocker:
php artisan blade-lint
On a mature codebase expect a handful of findings, and expect a few of them to be intentional (see the partials gotcha below). Fix the real errors, then decide which paths need excluding before you wire this into CI.
Where to look first in the source. The command class is small — read it before trusting the output. You want to know three things: what path(s) it scans by default, how it reports failures (does it exit non-zero?), and whether it maps error line numbers back to the .blade.php file or to the compiled PHP. That last detail determines how much of your day you'll spend hunting the wrong line.
First use case. The immediate win is catching malformed Blade in views that no test renders — admin screens, emails, error pages. A missing @endif in resources/views/emails/ currently fails silently until a real customer triggers it. This command turns that into a build failure.
Composer script as the single entry point. Don't let developers invent their own invocations:
"scripts": {
"lint:blade": "php artisan blade-lint",
"check": ["@lint:blade", "vendor/bin/pint --test", "vendor/bin/phpstan"]
}
Now composer check is the one command to run before pushing, and CI runs the identical thing.
Targeted runs during development. Linting every view on every save is slow in a large app. Lint what you touched — either by passing a path if your installed version supports it, or by looping over the changed files from git:
git diff --name-only --diff-filter=ACM | grep '\.blade\.php$' \
| while read -r f; do php artisan blade-lint "$f" || exit 1; done
Validate the argument form against --help — the exact signature varies between versions, and a silently-ignored argument gives you a green build for free.
CI as a dedicated fast job. Keep it separate from the test job so it fails in seconds, not minutes:
blade-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: '8.3' }
- run: composer install --no-interaction --prefer-dist
- run: php artisan blade-lint
The command needs a bootable app: an .env (or CI env vars) and an APP_KEY are usually enough. If your service providers touch the database or external services during boot, set APP_ENV=testing and supply a throwaway SQLite database.
Pre-commit hook for staged files. With CaptainHook or a plain .git/hooks/pre-commit, lint only staged Blade files. This is where the tool saves the most time — feedback arrives while the file is still in your head.
Run it in the image build, not just CI. Adding php artisan blade-lint && php artisan view:cache to your Dockerfile's production stage means a broken view can never reach a deployed artifact. view:cache alone would also surface the error, but it fails with a compiled-file path and no per-file summary; this command gives you a readable list.
Complement, don't replace, the test suite. The natural pairing is: this command for parse-time correctness on every view, and a smoke test that GETs each major route for render-time correctness. Neither covers the other's failure mode.
Custom wrapper if your CI needs structured output. If your pipeline wants annotations rather than plain text, write a thin Artisan command that walks the view directory and reuses Laravel's own compiler — the same trick the package uses:
$compiler = app('blade.compiler');
foreach (File::allFiles(resource_path('views')) as $file) {
if (! str_ends_with($file->getFilename(), '.blade.php')) continue;
$php = $compiler->compileString(File::get($file->getPathname()));
// write to a temp file, run `php -l`, parse the output
}
This is also your escape hatch if the package goes unmaintained.
It compiles; it does not execute. The check is essentially "does this Blade compile to syntactically valid PHP?" Undefined variables, missing components, bad prop names, wrong @include paths, and broken route calls all pass. Pair with Larastan/PHPStan at a high level and at least one render test per layout.
Line numbers may be off. If errors reference the compiled file (or line numbers that don't match your editor), cross-reference storage/framework/views or run php artisan view:cache and open the generated file. Getting this wrong costs an hour of staring at a correct-looking line 42.
Partials that span files will fail — incorrectly. A partial that opens @if/@section/@can and closes in the including file is legal at runtime but illegal in isolation. Either refactor to self-contained partials (worth doing anyway) or exclude those paths. Don't "fix" them by adding stub closers.
Custom directives depend on boot order. The lint command runs through Artisan, so Blade::directive(...) calls in service providers are registered — unless they're gated behind a request check, middleware, or an environment conditional. If you see false positives on a custom tag, that gate is the culprit.
Exclude generated and vendored views. Published package views under resources/views/vendor, IDE stubs, and scaffolding templates often contain intentionally odd Blade. Exclusions belong in one place (the command invocation in your Composer script or config, if publishable) so local and CI runs agree.
Verify exit codes actually fail the build. Introduce a deliberate @if( in a test view, run the command, and confirm a non-zero exit. Some versions print errors but exit 0, which makes the entire CI job decorative.
Licensing is worth a two-minute check. GPL-3.0 is unusual for a Laravel dev tool. Using it as a dev-only dependency that isn't redistributed is normally fine, but if you ship your application as a Docker image with vendor/, get a decision from whoever owns licensing before it becomes load-bearing in your pipeline.
Small package, low activity. At this size, pin the version in composer.json and be ready for the upstream to stall. Because the fallback is ten lines against BladeCompiler, this is a low-risk dependency — but keep the fallback in mind rather than assuming a fix will land.
Don't lint on every request-path trigger. Wiring this into a file watcher or php artisan serve loop will burn CPU and produce noisy duplicates of what your editor already shows. It's a pre-commit and pre-deploy tool.
How can I help you explore Laravel packages today?