Guides
Building a Blade Template Linter
Use Forte's findAll queries, element attribute access, and echo type checks to detect missing accessibility attributes, potential XSS vulnerabilities, inline styles, and deprecated HTML elements in Blade templates.
If you need a full-featured Blade linter rather than a purpose-built check, use Sheath. It adds a broad rule catalog, automatic fixes, baselines, inline suppressions, CI reporters, persistent caching, and first-party ecosystem plugins. Browse the complete rule catalog before building your own.
#Finding Images Without Alt Text
The findAll method accepts a callback and returns every node that matches. Combined with hasAttribute, you can find images that lack alternative text:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
$blade = <<<'BLADE'
<img src="/logo.png" alt="Logo">
<img src="/hero.jpg">
<img src="/icon.svg">
BLADE;
$doc = Forte::parse($blade);
$missing = $doc->findAll(
fn ($n) => $n instanceof ElementNode
&& $n->is('img')
&& ! $n->hasAttribute('alt')
);
count($missing); // 2
Each returned node carries position data. Use startLine and startColumn to report the exact location:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
$blade = <<<'BLADE'
<img src="/logo.png" alt="Logo">
<img src="/hero.jpg">
BLADE;
$doc = Forte::parse($blade);
$missing = $doc->findAll(
fn ($n) => $n instanceof ElementNode
&& $n->is('img')
&& ! $n->hasAttribute('alt')
);
$missing[0]->startLine(); // 2
$missing[0]->startColumn(); // 1
#Detecting Raw Echo Usage
Raw echoes ({!! !!}) bypass Blade's HTML escaping. While sometimes intentional, they can introduce XSS vulnerabilities. The isRaw method on EchoNode identifies them:
<?php
use Forte\Ast\EchoNode;
use Forte\Facades\Forte;
$blade = <<<'BLADE'
<p>{{ $name }}</p>
<div>{!! $html !!}</div>
<span>{!! $content !!}</span>
BLADE;
$doc = Forte::parse($blade);
$rawEchoes = $doc->findAll(
fn ($n) => $n instanceof EchoNode && $n->isRaw()
);
count($rawEchoes); // 2
$rawEchoes[0]->expression(); // "$html"
$rawEchoes[1]->expression(); // "$content"
#Finding Inline Event Handlers
Inline JavaScript event handlers such as onclick and onmouseover can violate
a Content Security Policy and create an injection risk. Define the event
attributes your rule rejects so ordinary attributes such as once are not
mistaken for handlers:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
$blade = <<<'BLADE'
<button onclick="handleClick()">Click</button>
<div onmouseover="highlight()">Hover</div>
<a href="/home">Safe Link</a>
<div once="setup">Not an event handler</div>
BLADE;
$doc = Forte::parse($blade);
$eventHandlers = [
'onclick',
'onmouseover',
'onfocus',
'oninput',
'onchange',
'onsubmit',
];
$violations = $doc->findAll(function ($node) use ($eventHandlers) {
if (! $node instanceof ElementNode) {
return false;
}
foreach ($node->getAttributes() as $attr) {
if (in_array(strtolower($attr->nameText()), $eventHandlers, true)) {
return true;
}
}
return false;
});
count($violations); // 2
#Finding Inline Styles
Some projects require every style declaration to live in CSS classes. Use
hasAttribute when you want a project-specific rule that flags inline style
attributes:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
$blade = <<<'BLADE'
<div style="color: red">Warning</div>
<p class="text-gray-600">Normal text</p>
<span style="font-weight: bold">Bold</span>
BLADE;
$doc = Forte::parse($blade);
$styled = $doc->findAll(
fn ($n) => $n instanceof ElementNode && $n->hasAttribute('style')
);
count($styled); // 2
$styled[0]->getAttribute('style'); // "color: red"
$styled[0]->tagNameText(); // "div"
#Finding Deprecated HTML Elements
Deprecated elements like <font>, <center>, and <strike> should be replaced with CSS. Check the tag name against a list of deprecated tags:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
$deprecated = ['font', 'center', 'strike', 'marquee', 'blink'];
$blade = <<<'BLADE'
<center>Centered Content</center>
<p>Normal paragraph</p>
<font color="red">Red text</font>
BLADE;
$doc = Forte::parse($blade);
$found = $doc->findAll(
fn ($n) => $n instanceof ElementNode
&& in_array($n->tagNameText(), $deprecated)
);
count($found); // 2
$found[0]->tagNameText(); // "center"
$found[1]->tagNameText(); // "font"
#Finding Empty Links
Anchor tags without visible text content create accessibility barriers for screen readers. Check for links where the inner content is empty or whitespace-only:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
$blade = <<<'BLADE'
<a href="/home">Home</a>
<a href="/empty"></a>
<a href="/icon"><img src="/icon.svg" alt="Icon"></a>
BLADE;
$doc = Forte::parse($blade);
$emptyLinks = $doc->findAll(
fn ($n) => $n instanceof ElementNode
&& $n->is('a')
&& trim($n->innerContent()) === ''
);
count($emptyLinks); // 1
#Composing Multiple Rules
For a reusable linter, define each rule as a function that returns an array of findings. Run all rules against a single parsed document:
<?php
use Forte\Ast\EchoNode;
use Forte\Ast\Elements\ElementNode;
use Forte\Ast\Node;
use Forte\Facades\Forte;
function findMissingAlt(array $nodes): array
{
return array_filter(
$nodes,
fn (Node $n) => $n instanceof ElementNode
&& $n->is('img')
&& ! $n->hasAttribute('alt')
);
}
function findRawEchoes(array $nodes): array
{
return array_filter(
$nodes,
fn (Node $n) => $n instanceof EchoNode && $n->isRaw()
);
}
function findInlineStyles(array $nodes): array
{
return array_filter(
$nodes,
fn (Node $n) => $n instanceof ElementNode
&& $n->hasAttribute('style')
);
}
$doc = Forte::parse($template);
$allNodes = $doc->findAll(fn () => true);
$issues = [];
foreach (findMissingAlt($allNodes) as $node) {
$issues[] = "Line {$node->startLine()}: Image missing alt attribute";
}
foreach (findRawEchoes($allNodes) as $node) {
$issues[] = "Line {$node->startLine()}: Raw echo {$node->expression()} may be an XSS risk";
}
foreach (findInlineStyles($allNodes) as $node) {
$issues[] = "Line {$node->startLine()}: Inline style on <{$node->tagNameText()}>";
}
This pattern scales well. Each rule is independent and testable on its own, and adding a new rule is a matter of writing one more filtering function.
#Scanning Multiple Files
To lint an entire project, combine Forte::parseFile with Laravel's recursive
File::allFiles method and keep Blade files:
<?php
use Forte\Ast\Elements\ElementNode;
use Forte\Facades\Forte;
use Illuminate\Support\Facades\File;
foreach (File::allFiles(resource_path('views')) as $file) {
if (! str_ends_with($file->getFilename(), '.blade.php')) {
continue;
}
$path = $file->getPathname();
$doc = Forte::parseFile($path);
$missing = $doc->findAll(
fn ($n) => $n instanceof ElementNode
&& $n->is('img')
&& ! $n->hasAttribute('alt')
);
foreach ($missing as $node) {
echo "{$path}:{$node->startLine()} - Image missing alt attribute\n";
}
}
#See Also
Continue with these related guides:
- Sheath: Adopt the full-featured Blade and HTML linter built on Forte
- Sheath Rules: Browse all 86 built-in rules
- Traversal: Navigate and query the document tree
- Elements: Full element and attribute API reference
- XPath Queries: Query documents with XPath expressions
- Documents: The Document API for parsing and rendering templates