spatie/laravel-blade-comments
Spatie's Laravel Blade Comments package wraps each rendered Blade view and component in HTML comments, so browser dev tools immediately reveal which view produced which markup, plus extra request info at the top of the document.
Install as a dev dependency so it never ships to production:
composer require spatie/laravel-blade-comments --dev
That's it — there is no service provider registration or publish step required to get value. The package ships with sensible defaults, and the shipped config is gated on env('APP_DEBUG') via 'enable' => env('APP_DEBUG'), so it activates automatically in local/staging debug environments and stays quiet elsewhere.
Publish the config only when you need to tune it:
php artisan vendor:publish --tag="blade-comments-config"
Where to look first:
<!-- blade: ... --> (or similar) comments wrapping each rendered view. That's the core behavior working.First use case: you're handed an unfamiliar codebase and need to change a small piece of markup — a breadcrumb, a card border, a footer link. Instead of grepping for class names across resources/views, right-click → Inspect → look at the wrapping comment → jump straight to the file.
Debugging a page assembled from many partials. The default blade_commenters list covers the directives you actually trip over: @extends, @include, @section/@yield, anonymous/class Blade components (<x-...>), and both Livewire components (<livewire:...>) and Livewire directives. So a page like:
@extends('layouts.app')
@section('content')
@include('partials.breadcrumbs')
<x-card>
<livewire:user-stats />
</x-card>
@endsection
produces comments delimiting each of those regions. You can walk the rendered source top-down and map the DOM structure to the view tree.
Component attribution. Since BladeComponentCommenter is registered, <x-...> usages are annotated. On a design-system-heavy app this is the single highest-value feature: the comment tells you the component's view path, not the call site, which is exactly what you want when the markup looks wrong but you can't tell which components/ file it came from.
Trimming noise with excludes. The two exclusion lists matter in real projects:
excludes.includes — for partials that are not HTML. A view that emits a <style> block, an inline <script>, a favicon link, or an SEO meta bundle. Comments injected around those either break parsing or look absurd inside <head>. Add the include name (e.g. 'styles.theme', 'partials.sidebar').excludes.sections — added in 1.4.0. Sections rendered into attributes or meta tags can't tolerate comment injection. If you have @yield('meta_description') inside a <meta content="..."> tag, exclude meta_description or you'll ship broken markup even in debug.Pattern: keep the exclude lists short and intentional. If you find yourself excluding a dozen includes, the problem is probably that those views shouldn't be includes in the first place.
Disabling per-environment. The config is env('APP_DEBUG')-driven, but you can pin it explicitly. Good practice when you have a shared staging environment where you want the comments on despite APP_DEBUG=false, or a debug-heavy local setup where you want them off:
'enable' => env('BLADE_COMMENTS_ENABLED', env('APP_DEBUG', false)),
Then set BLADE_COMMENTS_ENABLED=true in .env.staging.
Custom Blade commenters. The extension point is deliberately tiny — implement the interface and append to blade_commenters:
namespace App\Support;
use Spatie\BladeComments\Commenters\BladeCommenters\BladeCommenter;
class ViteEntryCommenter implements BladeCommenter
{
public function pattern(): string
{
return '/@vite\((.+?)\)/s';
}
public function replacement(): string
{
return '<!-- vite: $1 -->' . "\n" . '@vite($1)';
}
}
'blade_commenters' => [
// ... defaults
App\Support\ViteEntryCommenter::class,
],
Realistic uses: annotating @can/@role blocks with the ability name (so you know why a fragment is hidden), annotating @foreach origins, or tagging @env blocks. Because replacement is arbitrary string output, you can also strip directives you don't care about — the pattern/replacement pair is a general precompiler hook, not just a commenter.
Note the contract is pattern() + replacement() applied via preg_replace on the precompiled Blade source — match what the shipped commenters do (they're the reference implementations). Since 2.0 the package itself parses Blade with an AST, but your custom commenters still operate on the regex contract, so be careful your pattern doesn't accidentally match inside a string literal or a {{ }} expression.
Custom request commenters are the other extension point, and they run on the actual Request/Response objects:
class UserCommenter implements RequestCommenter
{
public function comment(Request $request, Response $response): ?string
{
$user = $request->user();
return $user
? "<!-- auth: {$user->id} ({$user->email}) -->"
: '<!-- auth: guest -->';
}
}
Returning null skips injection for that request. This is genuinely useful when debugging permission-gated markup — you see at the top of the response exactly who the request was evaluated as, rather than inferring it from rendered output.
Middleware ordering. AddRequestComments must be able to read the full response body, so it needs to run outside anything that streams or transforms output. The default middleware stack handles this. If you have a custom CompressResponse or streaming middleware, place AddRequestComments earlier so it sees uncompressed HTML.
The comments are in production if you're careless. --dev install plus ENABLE => env('APP_DEBUG') is the safe combination, but APP_DEBUG=true on a staging box that customers can reach leaks your entire view tree — file paths, component structure, Livewire component names, route names. Treat the config as a security-relevant setting and verify view-source: on any internet-facing environment before you consider it done.
Never enable this while debugging the frontend itself. Comments added before/after elements shift whitespace-sensitive layouts. Inline elements separated by whitespace (<a>x</a> <a>y</a>) are the classic casualty — the comment node can change how spaces collapse, producing a gap in debug that isn't in production. If a spacing bug only reproduces with the package on, that's why. Also breaks :empty, :nth-child, and :first-child selectors in some cases, and anything relying on innerHTML string comparison (JS template matching, snapshot tests, some hydration checks).
@yield inside attributes is the most common breakage. Comments wrapping a @yield('og_title') inside <meta property="og:title" content="..."> produce invalid HTML. The excludes.sections option exists exactly for this. When a page suddenly looks wrong after installing, check meta/attribute sections first.
Compiled views caching. Comments are injected at precompile time. After changing config (especially excludes or commenter lists) you may need php artisan view:clear before the change is reflected — stale compiled views will keep the old behavior. If it "won't turn off," this is almost always the cause.
Livewire adds a layer. LivewireComponentCommenter and LivewireDirectiveCommenter annotate the initial render, but subsequent Livewire DOM updates are generated from component render output and won't carry the same request-level header. For Livewire-heavy debugging, combine with Livewire's own dev tools and treat the blade comments as covering the initial page only.
@include with data and conditional includes have historically been the buggiest area (see 1.2.1/1.2.2 changelog entries). If you're on an older 1.x, upgrade — the 2.0 AST-based rewrite eliminates a whole class of regex false-positives, including matches inside strings. Anything below 2.0 is best avoided on views with complex @include($var, [...]) expressions.
Don't leave it on during performance work. Every comment adds bytes to the response and the precompilation pass runs on uncached views. If you're profiling TTFB or payload size, disable it first — otherwise you'll chase ghosts.
Test assertions will break. If you have feature tests asserting assertSee on exact markup, or snapshot tests of HTML fragments, they'll fail the moment this is enabled in the test environment. Keep APP_DEBUG=false in phpunit.xml (the Laravel default) and you won't notice; flip it and you'll get a wave of confusing diffs.
It doesn't replace a proper debugger. The package answers "which file?" — not "which variable?" or "which branch?". Pair it with @dump/x-ray/Telescope for the data side. Also remember Blade's own {{-- --}} comments are stripped before the browser sees them; this package's output is HTML comments that do ship, which is the whole point and also the whole risk.
How can I help you explore Laravel packages today?