Blade Templates
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Laravel 13.x (13.34.0) on PHP 8.3 to 8.5, checked on 2026-10-05. Every example is
rendered in the Bookshelf application used across this guide. Blade templates are .blade.php files in
resources/views, compiled once into plain PHP in storage/framework/views and recompiled when the source changes.
Components and layouts built from them are on Blade
Components and Layouts; the plain-PHP templates Blade replaces are on
Templating, Routing and Middleware by Hand.
A page and its layout
A route passes data to a view; the view extends a layout and fills its sections. This page and layout use most of the directives covered below:
<?php
// routes/catalogue.php
use App\Models\Book;
use Illuminate\Support\Facades\Route;
use Laravel\Head\Facades\Head;
Route::get('/shelf', function () {
Head::title('The shelf')->description('Every book in the Bookshelf catalogue.');
return view('shelf.index', [
'heading' => 'The shelf',
'blurb' => '<script>alert("xss")</script> & more',
'trustedHtml' => '<em>Staff pick</em>',
'books' => Book::orderBy('id')->get(),
'emptyShelf' => collect(),
'chosen' => 'paperback',
'inStock' => false,
'member' => 'student',
'showLegend' => true,
]);
});
{{-- resources/views/layouts/site.blade.php --}}
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
@head
@stack('styles')
</head>
<body>
<nav>Bookshelf · {{ $bookCount }} books</nav>
@session('status')
<p class="notice">{{ $value }}</p>
@endsession
<main>@yield('content')</main>
<aside>
@section('sidebar')
<p>Opening hours: 9 to 17</p>
@show
</aside>
@stack('scripts')
</body>
</html>
{{-- resources/views/shelf/index.blade.php --}}
@extends('layouts.site')
@inject('fines', 'App\Services\FineCalculator')
@section('content')
<h1>{{ $heading }}</h1>
<p>Escaped: {{ $blurb }}</p>
<p>Raw: {!! $trustedHtml !!}</p>
@unless ($books->isEmpty())
<ol>
@foreach ($books as $book)
@if ($loop->index >= 3)
@break
@endif
<li>{{ $loop->iteration }}/{{ $loop->count }} {{ $book->title }}@if ($loop->last) (last)@endif</li>
@endforeach
</ol>
@endunless
@forelse ($emptyShelf as $book)
<p>{{ $book->title }}</p>
@empty
<p>No books on this shelf.</p>
@endforelse
<ul>
@each('partials.book-row', $books->take(2), 'book')
</ul>
<select name="format">
@foreach (['hardcover', 'paperback', 'ebook'] as $format)
<option value="{{ $format }}" @selected($format === $chosen)>{{ ucfirst($format) }}</option>
@endforeach
</select>
<input type="checkbox" name="in_stock" @checked($inStock)> <button @disabled(! $inStock)>Borrow</button>
@switch($member)
@case('staff')
<p>Loan period: 60 days</p>
@break
@default
<p>Loan period: 28 days</p>
@endswitch
@auth <p>Signed in</p> @endauth
@guest <p>Guest: fine for 4 days late is {{ $fines->fineFor(4) }} cents</p> @endguest
@env('local') <p>Local environment</p> @endenv
@isset($missing) <p>never shown</p> @endisset
@php($total = $books->count())
<p>{{ $total }} books in total</p>
@includeWhen($showLegend, 'partials.legend', ['legend' => 'old = before 1980'])
@once
@push('scripts')
<script>window.shelf = @json(['books' => $total, 'note' => '</script>']);</script>
@endpush
@endonce
@verbatim
<p>{{ Rendered by Vue, not Blade }}</p>
@endverbatim
@endsection
@section('sidebar')
@parent
<p>Returns go in the box by the door.</p>
@endsection
{{-- resources/views/partials/book-row.blade.php --}}
<li @class(['book', 'first' => $loop?->first ?? false, 'old' => $book->published_year < 1980])>
{{ $book->title }} ({{ $book->published_year ?? 'n.d.' }})
</li>
The response, with blank lines removed:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The shelf - Bookshelf</title>
<meta name="description" content="Every book in the Bookshelf catalogue."> </head>
<body>
<nav>Bookshelf · 26 books</nav>
<main> <h1>The shelf</h1>
<p>Escaped: <script>alert("xss")</script> & more</p>
<p>Raw: <em>Staff pick</em></p>
<ol>
<li>1/26 Kindred</li>
<li>2/26 Parable of the Sower</li>
<li>3/26 The Dispossessed</li>
</ol>
<p>No books on this shelf.</p>
<ul>
<li class="book old">
Kindred (1979)
</li>
<li class="book">
Parable of the Sower (1993)
</li>
</ul>
<select name="format">
<option value="hardcover" >Hardcover</option>
<option value="paperback" selected>Paperback</option>
<option value="ebook" >Ebook</option>
</select>
<input type="checkbox" name="in_stock" > <button disabled>Borrow</button>
<p>Loan period: 28 days</p>
<p>Guest: fine for 4 days late is 100 cents</p> <p>Local environment</p>
<p>26 books in total</p>
<p class="legend">old = before 1980</p>
<p>{{ Rendered by Vue, not Blade }}</p>
</main>
<aside>
<p>Opening hours: 9 to 17</p>
<p>Returns go in the box by the door.</p>
</aside>
<script>window.shelf = {"books":26,"note":"<\/script>"};</script>
</body>
</html>
Source: Laravel — Passing Data to Views
Points the output shows:
-
@yield('content')printed the page’s section;@section('sidebar') ... @showdefined a default that the page extended with@parent;@stack('scripts')collected the@pushfrom the page, which@oncekeeps to one copy even if the partial is included many times. -
$loopgivesindex,iteration,count,first,last,even/odd,depthandparentinside@foreach;@breakand@continuetake an optional condition.@eachrenders a partial per item but provides no$loop:'first' => $loop?->firstnever applied to Kindred. -
@selected,@checkedand@disabledprint the bare attribute when the condition is true;@classbuilds theclassattribute from conditions (and@stylethestyleattribute). -
@session('status')(Laravel 10) renders only when the session has that key, with the value in$value; it printed nothing because nothing was flashed. -
@verbatimleft{{ }}untouched for a front-end framework;@{{ name }}does the same for one expression. -
$bookCountcame from a view composer and the<title>from Laravel Head, both described below.
Escaped output, raw output and XSS
{{ $value }} compiles to <?php echo e($value); ?>: e() calls htmlspecialchars() with ENT_QUOTES and UTF-8
(and leaves Htmlable objects such as HtmlString alone). {!! $value !!} echoes unchanged. The compiled view in
storage/framework/views shows it:
<?php
// storage/framework/views/<hash>.php (excerpt, compiled from shelf/index.blade.php)
?>
<p>Escaped: <?php echo e($blurb); ?></p>
<p>Raw: <?php echo $trustedHtml; ?></p>
<option value="<?php echo e($format); ?>" <?php if($format === $chosen): echo 'selected'; endif; ?>><?php echo e(ucfirst($format)); ?></option>
{!! !!} is for HTML the application produced or sanitized itself (Markdown rendered by Str::markdown() with
html_input => 'strip', for example), never for user input. Escaping for HTML text and attributes does not make a
value safe in every context: inside href a javascript: URL still runs, so validate URLs; inside <script> use
JSON encoding, next section. The contexts are explained on
Forms, Validation and Output Escaping.
JSON for JavaScript: @json and Js::from
Data for a script should be JSON-encoded with the HTML-safe flags. Three ways, rendered with Blade::render():
<?php
use Illuminate\Support\Facades\Blade;
$data = ['note' => "</script><b>'hi'</b>"];
echo Blade::render('A: @json($data)', ['data' => $data]), PHP_EOL;
echo Blade::render("B: @json(['note' => \"</script><b>'hi'</b>\", 'n' => 1])"), PHP_EOL;
echo Blade::render('C: {{ Js::from($data) }}', ['data' => $data]), PHP_EOL;
A: {"note":"<\/script><b>'hi'<\/b>"}
B: {"note":"<\/script><b>'hi'<\/b>","n":1}
C: JSON.parse('{"note":"\\u003C\\\/script\\u003E\\u003Cb\\u003E\\u0027hi\\u0027\\u003C\\\/b\\u003E"}')
Source: Laravel — Rendering JSON
A used @json on a variable and got JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT: no <, >,
' or & survives. B passed an array literal, and the escaping flags were lost. @json splits its argument on
commas to find optional flags and depth, so the array’s own commas were taken as separators and <b> came through
unescaped; only PHP’s default \/ escaping kept </script> from closing the tag. The documentation’s advice to pass
@json only existing variables is a security rule, not style. Js::from() (C) produces a JSON.parse('...')
expression that is also safe inside an HTML attribute such as Alpine’s x-data.
Includes, forms and service injection
| Directive | Use |
|---|---|
|
Render a partial with the parent’s variables plus the array; |
|
A partial per item, with an optional view for an empty collection; no |
|
The hidden |
|
The first validation message for a field; |
|
Resolve a service from the container into |
|
Raw PHP and imports; a sign that logic should move to the controller or a component when it grows. |
Source: Laravel — Including Subviews, Forms and Service Injection
Fragments and inline templates
@fragment('rows') ... @endfragment marks part of a view that a controller can return on its own, which is how
htmx and Turbo update part of a page without a JSON API:
{{-- resources/views/shelf/list.blade.php --}}
<h1>Books</h1>
<input type="search" hx-get="/shelf/list" hx-target="#rows">
<ul id="rows">
@fragment('rows')
@foreach ($books as $book)
<li>{{ $book->title }}</li>
@endforeach
@endfragment
</ul>
<?php
// routes/catalogue.php
use App\Models\Book;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/shelf/list', function (Request $request) {
$books = Book::where('title', 'like', '%'.$request->string('q').'%')->orderBy('title')->limit(3)->get();
return view('shelf.list', ['books' => $books])->fragmentIf($request->hasHeader('HX-Request'), 'rows');
});
$ curl -s 'localhost:8000/shelf/list?q=the' # whole page (whitespace trimmed)
<h1>Books</h1>
<input type="search" hx-get="/shelf/list" hx-target="#rows">
<ul id="rows">
<li>Parable of the Sower</li>
<li>The Dispossessed</li>
<li>The Lathe of Heaven</li>
</ul>
$ curl -s -H 'HX-Request: true' 'localhost:8000/shelf/list?q=the' # the fragment only
<li>Parable of the Sower</li>
<li>The Dispossessed</li>
<li>The Lathe of Heaven</li>
Blade::render('Hello {{ $name }}', ['name' => $name]) compiles and renders a template string, used above for the
JSON examples; it writes a compiled file per distinct string, so it is for occasional templates (a CMS snippet), not
for strings built per request.
Custom directives and conditions
Blade::directive() maps @name(expression) to PHP code at compile time; Blade::if() defines a condition with
@name, @elsename, @else and @endname. Both are registered in a service provider’s boot():
<?php
// AppServiceProvider::boot() (run here with Blade::render)
use App\Models\Book;
use Illuminate\Support\Facades\Blade;
Blade::if('lent', fn (Book $book) => $book->reviews()->count() > 0);
echo Blade::render('Hello {{ $name }}, @lent($book) reviewed @else not reviewed @endlent',
['name' => '<Ana>', 'book' => Book::first()]);
// Hello <Ana>, reviewed
Source: Laravel — Custom If Statements and Extending Blade
A directive’s closure receives the expression as source code, not as a value, and runs once at compile time; its
output must be PHP that does the work at render time, and it must escape what it echoes. After changing a directive,
php artisan view:clear forces recompilation.
View composers, sharing and caching
A view composer runs whenever a view is rendered and adds data to it, so a layout’s data (the 26 books in the
navigation) does not have to be passed by every controller:
<?php
// app/View/Composers/BookCountComposer.php
namespace App\View\Composers;
use App\Models\Book;
use Illuminate\View\View;
/** Runs every time the layout is rendered, wherever the page comes from. */
class BookCountComposer
{
public function compose(View $view): void
{
$view->with('bookCount', Book::count());
}
}
// AppServiceProvider::boot()
// View::composer('layouts.site', BookCountComposer::class);
Source: Laravel — View Composers
View::composer(['a', 'b'], ...) or 'partials.*' attaches one composer to several views; View::creator() runs
when the view is instantiated, before composers; View::share('key', $value) in boot() gives every view the same
value. A composer that queries the database runs on every render of its view, including error pages that use the
layout, so keep it cheap or cache the value.
php artisan view:cache compiles every template up front (part of php artisan optimize in a deployment) and
view:clear deletes the compiled files. In development Blade compares modification times and recompiles changed
views automatically.
Page metadata with Laravel Head
laravel/head (0.2.2 when this page was checked, so still pre-1.0) manages the document <head>: title, description,
canonical URL, robots, Open Graph and X cards, theme colours and JSON-LD schemas. Defaults go in a service provider,
the page sets its own values at runtime, and @head renders the merged result:
<?php
// AppServiceProvider::boot()
use Laravel\Head\Facades\Head;
use Laravel\Head\HeadBuilder;
Head::defaults(fn (HeadBuilder $head) => $head
->title('Bookshelf', suffix: ' - Bookshelf')
->description('A small library catalogue.'));
// in the /shelf route
Head::title('The shelf')->description('Every book in the Bookshelf catalogue.');
The page’s <title>The shelf - Bookshelf</title> and description in the output above came from these two calls,
with the runtime values replacing the defaults field by field and the suffix kept. Route-level metadata, error-page
metadata and the Livewire and Inertia integrations are in the documentation.
Testing views
<?php
// tests/Feature/BladeTest.php
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('escapes echoed values', function () {
$this->blade('<p>{{ $blurb }}</p>', ['blurb' => '<script>alert(1)</script>'])
->assertSee('<script>', escape: false)
->assertDontSee('<script>', escape: false);
});
it('renders the shelf with the layout, the composer and Laravel Head', function () {
$this->seed();
$this->get('/shelf')
->assertOk()
->assertSee('<title>The shelf - Bookshelf</title>', escape: false)
->assertSeeInOrder(['Bookshelf ·', 'The shelf', 'No books on this shelf.', 'Returns go in the box']);
});
$ php artisan test --filter=BladeTest
PASS Tests\Feature\BladeTest
✓ it escapes echoed values 0.17s
✓ it renders the shelf with the layout, the composer and Laravel Head 0.05s
Tests: 2 passed (5 assertions)
Duration: 0.28s
Source: Laravel — Creating and Rendering Views and HTTP Tests
assertSee() escapes its argument by default, which is why the raw-HTML checks pass escape: false.
Where older books and tutorials are outdated
-
{{{ $value }}}for escaping and{{ }}for raw output (Laravel 4) were reversed in Laravel 5: today{{ }}escapes and{!! !!}is raw. -
json_encode()inside<script>without the HTML-safe flags, and@jsonwith array literals, should be@jsonon a variable orJs::from(). -
Manual
class="{{ $active ? 'active' : '' }}"and{{ $x ? 'checked' : '' }}are replaced by@class,@checked,@selectedand@disabled(all documented by Laravel 9). -
Layouts with
@extendseverywhere still work, but new applications and the starter kits use component layouts (<x-layouts.app>), covered on Blade Components and Layouts. -
Hand-built
<title>and meta tag sections in each layout can be replaced by Laravel Head, which also covers Open Graph and structured data.