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: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt; &amp; 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>

Points the output shows:

  • @yield('content') printed the page’s section; @section('sidebar') ... @show defined a default that the page extended with @parent; @stack('scripts') collected the @push from the page, which @once keeps to one copy even if the partial is included many times.

  • $loop gives index, iteration, count, first, last, even/odd, depth and parent inside @foreach; @break and @continue take an optional condition. @each renders a partial per item but provides no $loop: 'first' => $loop?->first never applied to Kindred.

  • @selected, @checked and @disabled print the bare attribute when the condition is true; @class builds the class attribute from conditions (and @style the style attribute).

  • @session('status') (Laravel 10) renders only when the session has that key, with the value in $value; it printed nothing because nothing was flashed.

  • @verbatim left {{ }} untouched for a front-end framework; @{{ name }} does the same for one expression.

  • $bookCount came 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"}')

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

@include('partials.legend', ['legend' => ...])

Render a partial with the parent’s variables plus the array; @includeIf skips a missing view, @includeWhen / @includeUnless take a condition, @includeFirst([...]) uses the first view that exists.

@each('partials.book-row', $books, 'book', 'partials.empty')

A partial per item, with an optional view for an empty collection; no $loop.

@csrf, @method('PUT')

The hidden _token and _method fields every non-GET form needs (Validation and Form Requests has a full form).

@error('title') {{ $message }} @enderror

The first validation message for a field; @error('title', 'login') reads a named error bag.

@inject('fines', 'App\Services\FineCalculator')

Resolve a service from the container into $fines, as the shelf page did to print 100 cents.

@php ... @endphp, @php($x = 1), @use('App\Models\Book')

Raw PHP and imports; a sign that logic should move to the controller or a component when it grows.

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 &lt;Ana&gt;, reviewed

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);

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('&lt;script&gt;', 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

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 @json with array literals, should be @json on a variable or Js::from().

  • Manual class="{{ $active ? 'active' : '' }}" and {{ $x ? 'checked' : '' }} are replaced by @class, @checked, @selected and @disabled (all documented by Laravel 9).

  • Layouts with @extends everywhere 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.