portbay / blade-stamper
Dev-time source-location stamping for Laravel Blade templates — data-pb-loc for PortBay's visual editor.
Requires
- php: >=8.1
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- illuminate/view: ^11.0 || ^12.0 || ^13.0
README
Dev-time Laravel Blade source-location stamping for PortBay's visual editor.
It stamps each rendered host element with its authored source location,
data-pb-loc="<file>:<line>:<col>", so a clicked element resolves back to the
exact span in your .blade.php file instead of a text-search guess.
<button data-pb-loc="resources/views/home.blade.php:42:7">Get started</button>
This is the Blade sibling of @portbay/swc-plugin-loc, and
it emits the same data-pb-loc shape, so PortBay's resolver handles Blade with
no client changes. With the attribute present you get precise text, class and
attribute editing, plus structural editing. Without it, PortBay falls back to
text search with no change in behaviour.
How it works (and why it needs no source-map)
Blade compiles server-side, so there is a point where the raw template text
is available with its original line numbers. The stamper registers as the
first Blade compilation pass via Blade::prepareStringsForCompilationUsing,
which runs before Blade mutates the string (before comment stripping, @php
extraction and <x-component> rewriting). Coordinates are read from the pristine
source and baked in as literal attribute strings. Nothing has to be traced back
from compiled output.
A Blade-aware masking pass blanks every code region: {{ }} / {!! !!}
echoes, {{-- --}} comments, <?php … ?> blocks, @php … @endphp,
@verbatim, and @directive(...) arguments, so a stray < inside a PHP
expression is never mistaken for an HTML tag. The mask only guides where not to
stamp; inserts are applied to the original text. Over-masking can therefore only
cause a tag to fall back to text-search (safe) and can never corrupt a template.
Blade component tags (<x-...>, <x-slot>) are intentionally not stamped:
their attributes go to the component's attribute bag, not a literal output tag.
Their internals are stamped when their own view is compiled.
Install
composer require --dev portbay/blade-stamper
Laravel package auto-discovery registers the service provider, so no config change needed. Restart your dev server and clear the compiled views if they were cached:
php artisan view:clear
Dev-only, by design
The stamper runs only outside production, checking app()->environment(). Leave
it alone and nothing reaches a production render: no extra DOM, no source paths
in shipped HTML.
PORTBAY_LOC=1overrides that check, andPORTBAY_LOC=0disables the stamper anywhere. The override exists so you can debug the stamper in an environment that reports itself as production, and it is not safe to leave on. Anything you serve with it set publishes your view file names, your directory layout and your line numbers to whoever loads the page. Use it on a machine you control, then unset it.
PORTBAY_LOC and php artisan serve
Put PORTBAY_LOC in your .env, not in the shell. php artisan serve
deletes every environment variable that is not on Laravel's own
ServeCommand::$passthroughVariables list (APP_ENV, PATH, the Herd and
Xdebug variables, and little else) before it starts the built-in web server, so
PORTBAY_LOC=0 php artisan serve # does NOT disable the stamper
is a silent no-op: the server process never sees the variable and the stamper
keeps emitting. PORTBAY_LOC=0 in .env works, and so does
php artisan serve --no-reload, which passes the whole environment through.
Under php-fpm, nginx, Octane, Sail or Herd the shell variable is honoured
normally — this is an artisan serve behaviour, not a stamper one.
Requirements
- PHP >= 8.1
- Laravel 11, 12 or 13 (
illuminate/view^11 || ^12 || ^13).prepareStringsForCompilationUsingarrived in Laravel 10.15; on an older compiler the provider no-ops and PortBay's text-search resolver stays in effect.
Verified
Against real scaffolded apps, on the served HTML rather than on unit tests
alone: Laravel 11.55.1, 12.66.0 and 13.25.0 (PHP 8.3.33) all serve
byte-identical stamped markup, and every emitted data-pb-loc was checked back
against the authored .blade.php at that exact line and column.
Scope: your views only
Only views that resolve inside your application's own view roots are
stamped. A framework or package view gets no attribute at all — no partial
coordinate, no placeholder — so a Laravel error page comes back with zero
data-pb-loc, and a click in PortBay can never land in installed package
source.
The roots come from Laravel's view finder (View::getFinder()->getPaths() —
config('view.paths') plus anything View::addLocation() registered), never
from matching vendor/ in the path string, because the string lies both ways:
resources/views/vendor/acme/alert.blade.php— a package view you published into your own app — is yours and stamps,vendor/in the path and all;- a package installed through a Composer
pathrepository is symlinked, so__DIR__resolves it somewhere with novendor/in the path at all, and it still is not yours. It does not stamp.
Being a namespaced view is not itself disqualifying — what counts is where the
file lives. Your own resources/views/errors/500.blade.php renders through the
errors:: namespace and stamps normally.
If the view roots cannot be read at all, nothing is stamped. An attribute pointing at the wrong file is worse than no attribute: without one, PortBay's text-search resolver still finds the element.
Test
composer test # php test/StampTest.php, pure-PHP unit tests, no Laravel needed
License
MIT © Tribal House LLC. Independent implementation; no third-party plugin code.