Skip to content
beginner Phase 13 · Laravel Framework Fundamentals

Blade Templates and Layouts

Render views with escaping and reusable layouts.

50m
0 problems
Topic Progress 0%

The XSS Boundary: {{ }} Escapes, {!! !!} Does Not

A Blade template is a text file with directives that compile to plain PHP. Controllers from Routing and Controllers return a view, and that view is a Blade file. The single echo you will type thousands of times is {{ }}, and its whole job is output escaping:

// in a Blade file:
{{ $user->name }}

// compiles to (roughly):
<?php echo e($user->name); ?>

e() is a helper around PHP's htmlspecialchars with ENT_QUOTES set, so <, >, &, and both quote characters become HTML entities. That is the difference between a profile name of <script>alert(1)</script> rendering as harmless text and executing in every visitor's browser. {{ }} escaping on by default is the single reason Laravel apps have a much smaller XSS surface than hand-rolled PHP templates where every echo must be remembered as htmlspecialchars(...).

The counterpart {!! !!} prints raw HTML with no escaping:

{!! $post->body !!}   // trust this only if you own the HTML

Use it when the content is genuinely markup you generated or sanitized — rendered Markdown, a CMS field that passes through an allowlist sanitizer like HTMLPurifier, or an email-builder fragment. Never point it at user-submitted text that skipped sanitization. The mental model to carry into interviews: {{ }} is for data, {!! !!} is for trusted markup, and the trust boundary is a decision you make per field, not per application.

Blade gives you two more output tools that sit between those extremes. @json($data) safely embeds a PHP structure into a <script> tag for a JavaScript consumer — it escapes <, >, &, and Unicode line separators so the string cannot break out of the script context:

<script>
  window.__PRODUCT__ = @json($product);
</script>

And for attributes you often want a conditional presence rather than an echo:

<input type="checkbox" name="active" value="1" @checked(old('active', $user->active))>
<img src="{{ $user->avatar_url }}" alt="{{ $user->name }}" @disabled(! $user->avatar_url)>

The @checked, @selected, @disabled, @readonly, and @required helpers render the matching attribute only when the expression is true — cleaner than an inline @if around a single attribute, and they read correctly in review.

Where does the compilation actually happen? On first render, Blade compiles every template into a plain PHP file under storage/framework/views (a hashed filename per template) and includes that on subsequent requests. You can open the compiled file to see exactly what a directive produces — a debugging technique that ends most "what does @forelse really do?" confusion in one look. Editing a .blade.php file is picked up immediately in development because Blade checks the source timestamp; in production you run php artisan view:cache so the compile step happens at deploy time, not on a user's first request.

Escaping has a wrinkle worth knowing for interviews: double encoding. If a value already contains entities — say a legacy CMS stored &amp; — passing it through {{ }} encodes the ampersand again and displays &amp; literally to the user. The fix is not to switch to {!! !!} on that field; it is to store and submit clean text, and let Blade escape exactly once at the output layer. Related: when you escape for HTML but then interpolate the same value into a JavaScript string or a URL, the context changed and so must the escaping. For URLs, the url() and route() helpers plus rawurlencode handle it; for JS contexts, prefer passing data through @json or a data attribute that JavaScript reads with dataset, rather than string-splicing into a script.

A common interview follow-up: does Blade escaping apply to attribute contexts too? Yes — {{ }} inside an attribute quotes the escaped value, which is why value="{{ $x }}" is safe even when $x contains quotes. The remaining attribute-context risk is unquoted attributes (value={{ $x }}), which Blade cannot protect reliably — always quote your attributes. One legitimate raw output you will meet early is form builders like Laravel Collective, which emit raw HTML intentionally; the modern replacements are plain Blade forms with @error and old(), or Livewire/Alpine for interactivity, and they follow the same escape-by-default rule you have just seen.

One Layout, Many Pages: @extends, @section, and Components

Every page shares the shell — nav, footer, <head> assets. Blade's layout mechanism keeps that shell in one file and lets each page fill in the slots.

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <title>@yield('title', config('app.name'))</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
    @include('partials.nav')

    <main class="container">
        @yield('content')
    </main>

    @stack('scripts')
</body>
</html>

A page consumes it by extending and filling sections:

{{-- resources/views/orders/index.blade.php --}}
@extends('layouts.app')

@section('title', 'Your orders')

@section('content')
    <h1>Orders</h1>
    @foreach ($orders as $order)
        <p>{{ $order->reference }} — {{ $order->total_cents / 100 }}</p>
    @endforeach
@endsection

@extends names the parent layout; @section('name', 'value') sets a short value, while @section('name') ... @endsection wraps a block. @yield('title', 'default') declares where a section lands and supplies a fallback — so a page that never sets title still renders a sensible <head>. @stack('scripts') plus @push('scripts') in a child lets any page append extra blocks to the parent, which is how page-specific JavaScript reaches the shell without conditionals in the layout.

For fragments reused across layouts, @include renders another view in the current context:

@include('partials.nav', ['active' => 'orders'])

All parent data is available inside the included view unless you restrict it — convenient, but it also means an @include can hide a data dependency. Anonymous components make the dependency explicit instead:

{{-- resources/views/components/alert.blade.php --}}
@props(['type' => 'info', 'message'])

<div {{ $attributes->merge(['alert' => true, 'alert-' . $type => true]) }}>
    {{ $message }}
</div>

{{-- usage anywhere --}}
<x-alert type="danger" :message="$error" class="mt-4" />

@props declares what the component accepts; extra attributes like class land in $attributes, and ->merge() combines your defaults with the caller's additions — the standard way to let callers add utility classes without the component knowing them. A @props-declared component takes data only through its attributes, which is the documentation value: you can see a component's entire interface at the call site.

When a view tree grows past what @include chains can express, components (class-based ones with a render() method, or Blade + Livewire/Volt) are the next step — but the trio of @extends, @include, and @stack covers most CRUD applications on its own.

A detail that surprises people about @include: the included view receives every variable available in the parent, plus whatever you pass explicitly. That convenience has a cost — a shared layout can accidentally depend on a variable only one page provides, and the failure appears as an "Undefined variable" error on that page alone. Components fix this by declaring their inputs with @props, so the data contract is visible at the call site and enforced when the component renders. When you review a template full of @include calls, each one is an undeclared dependency; converting frequently-used includes into components is a small refactor with a large readability payoff.

Layout inheritance also stacks. A page can extend a mid-level layout that itself extends the base shell — useful when one section of the app (say, the admin area) shares a sidebar that the public pages do not. The child-most view fills the sections it knows about, and intermediate layouts forward the rest with their own @extends and @yield. Keep the stack shallow though: three levels of inheritance is already hard to trace in a debugger, and at that point explicit components communicate better than nested yields. For assets, @vite(['resources/css/app.css', 'resources/js/app.js']) replaces the old asset() calls for modern builds and correctly points at the development server or the hashed production manifest.

Forms That Survive CSRF and Method Spoofing: @csrf, @method, old(), @error

HTML forms can only send GET and POST. Laravel's routes speak PUT, PATCH, and DELETE. The bridge is method spoofing, and every Blade form that mutates data needs three pieces:

<form method="POST" action="{{ route('orders.update', $order) }}">
    @csrf
    @method('PUT')

    <label for="reference">Reference</label>
    <input type="text" id="reference" name="reference" value="{{ old('reference', $order->reference) }}">

    <button type="submit">Save</button>
</form>

@csrf renders a hidden _token input bound to the session. The ValidateCsrfToken middleware rejects any state-changing request without a matching token — the check that stops a malicious page from posting to your endpoint using a visitor's session. @method('PUT') renders a hidden _method input; Symfony's method-parameter override — switched on when Laravel boots via Request::capture() — reads it and rewrites the request's verb before your route ever matches. A form missing @csrf fails with a 419 token mismatch; a form missing @method lands on a 405 because PUT routes never match a POST. Those two error codes diagnose half of all "my form is broken" reports.

Redirects and re-population close the loop. When validation fails, the controller redirects back with errors and the old input; the form repopulates with old():

value="{{ old('reference', $order->reference) }}"

The first argument is the previously submitted value, the second the fallback for a fresh page load. Forgetting the fallback is why a create form mysteriously clears on every validation error.

Error display has a blunt tool and a precise one. The blunt tool iterates the shared $errors bag:

@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

The precise tool attaches the message to its field:

<input type="email" name="email" value="{{ old('email') }}">
@error('email')
    <span class="text-red-600">{{ $message }}</span>
@enderror

@error('email') renders only when that field failed, with $message already localized and ready for custom attributes. The $errors bag itself is shared to every view by the ShareErrorsFromSession middleware in the web group — which is why it exists in templates even when your controller never passed it explicitly.

Two more details round out the pattern. Checkboxes submit nothing when unchecked, so pair them with a hidden sibling (<input type="hidden" name="newsletter" value="0"> before the checkbox) so the key always arrives. And for large forms, a name="items[0][sku]" nesting pattern lets one form submit an array that PHP parses into nested $_POST structure — Eloquent's create() and update() accept it directly. With forms rendering safely, the next topic puts a data layer under them: Querying with Eloquent ORM.

The full failed-validation flow deserves one clean walkthrough because it ties the whole topic together. The browser POSTs the form; ValidateCsrfToken checks the token; the controller's $request->validate() (or a FormRequest) runs; on failure Laravel throws a ValidationException, which the handler converts to a redirect back to the referring page with the errors and the old input flashed into the session; the browser follows the redirect with a GET; the template reads old() to repopulate and @error to annotate. Every piece you have seen — @csrf, @method, old(), $errors — exists to make that loop work. When the loop breaks, identify which link failed: a 419 is the token, a 405 is the method, an empty form is old(), and unstyled errors are the $errors bag never arriving because the redirect did not actually come from a failed validation.

File uploads ride the same rails with two additions: enctype="multipart/form-data" on the form tag so the browser streams file bytes instead of url-encoding them, and $request->file('avatar') (or validated('avatar')) in the controller returning an UploadedFile instance you move with ->store('avatars', 'public'). Validation rules like 'avatar' => ['required', 'image', 'max:2048'] treat the upload like any other field, and the @error directive annotates it the same way — the CSRF and method-spoofing machinery does not change at all.

Quiz

1. A comment field is rendered with {!! $comment->body !!} and users can post arbitrary text. What is the consequence, and when is the raw echo justified?

Question 1 options

2. A Laravel form submits but the route returns 405 Method Not Allowed even though the action URL is correct. The form's method attribute says POST and the route is Route::put(...). What is missing?

Question 2 options

3. In a template, {{ $user->name }} outputs the four characters &lt;b&gt; rather than bold text. What is happening?

Question 3 options

4. A controller redirects back after failed validation, but the form fields render empty even though the user typed values. What is the most likely cause?

Question 4 options

Flashcards

Question

What does {{ $value }} compile to, and why?

Answer

Roughly <?php echo e($value); ?> — e() is htmlspecialchars with ENT_QUOTES, so <, >, &, and quotes become entities. Escaping by default means user data displayed through {{ }} cannot inject markup, which is the primary XSS defense in Blade.

Question

When is {!! $value !!} appropriate, and what risk does it carry?

Answer

Only for HTML you generated or sanitized (rendered Markdown, purifier-passed CMS content). It skips escaping entirely, so any unsanitized user input becomes stored XSS executing in every viewer's browser. Data goes through {{ }}; trusted markup through {!! !!}.

Question

How do @extends, @section, and @yield cooperate to build a layout?

Answer

A child calls @extends('layouts.app') and fills sections with @section('name') ... @endsection (or @section('name', 'value') for short output). The parent declares slots with @yield('name', 'default') — the default renders when the child sets nothing. @stack/@push let children append extra blocks like scripts.

Question

What do @csrf and @method render, and which middleware consumes them?

Answer

@csrf renders a hidden _token input matched against the session by ValidateCsrfToken (missing token = HTTP 419). @method('PUT') renders a hidden _method input that the _method field Symfony's method-parameter override reads to rewrite the request verb, letting HTML forms reach PUT/PATCH/DELETE routes (missing = HTTP 405).

Question

Where does $errors come from in Blade, and what do old() and @error do?

Answer

The ShareErrorsFromSession middleware (web group) shares the error bag with every view. old('field', $default) reads validation-flashed input so forms repopulate after a redirect-back; @error('field') renders its block only when that specific field failed, exposing a localized $message.

Revision Notes

Key Takeaways

  • 1. Blade compiles to cached PHP under storage/framework/views; {{ }} compiles to e(), which escapes quotes and angle brackets so user data cannot inject markup
  • 2. {!! !!} is the XSS boundary: use it only for HTML you own or sanitized; @json safely embeds data in script tags
  • 3. @extends/@section/@yield define a shared layout with defaults; @include and anonymous x-components compose fragments, with @props declaring each component's interface
  • 4. @csrf renders the _token that ValidateCsrfToken checks (419 on failure) and @method spoofs PUT/PATCH/DELETE that HTML forms cannot send natively (405 on failure)
  • 5. $errors is shared by middleware; old() repopulates inputs after redirect-back and @error attaches localized messages to the exact field

Interview Tips

  • • "How does Blade prevent XSS?" — {{ }} compiles to e()/htmlspecialchars with ENT_QUOTES, escaping by default; name {!! !!} as the deliberate escape hatch and say when it is safe
  • • "A form gives 419 — what happened?" — the CSRF token was missing or did not match the session; mention @csrf, the token's session origin, and that state-changing routes live in the web group
  • • "Explain method spoofing" — HTML sends only GET/POST, @method renders _method, MethodSpoofing middleware rewrites the verb before routing; add that true API endpoints take real verbs instead
  • • "Where do $errors and old() come from?" — ShareErrorsFromSession middleware flashes both the error bag and old input on redirect-back; then describe @error and the old('field', $default) two-argument pattern

Cheat Sheet

Blade Templates and Layouts — Cheat Sheet

Output:

Syntax Escapes? Use for
{{ $x }} yes (e() = htmlspecialchars) all data, especially user input
{!! $x !!} no trusted/sanitized HTML only
@json($x) yes (script-safe) embedding data for JS
@checked/@selected/@disabled n/a conditional attributes

Layouts:

{{-- parent --}}
@yield('title', 'default')
@yield('content')
@stack('scripts')
@include('partials.nav')

{{-- child --}}
@extends('layouts.app')
@section('content') ... @endsection
@push('scripts') ... @endpush

Components: <x-alert type="danger" :message="$error" /> → components/alert.blade.php with @props(['type' => 'info', 'message']); $attributes->merge([...]) lets callers add classes.

Forms:

<form method="POST" action="{{ route('orders.update', $order) }}">
    @csrf
    @method('PUT')
    <input name="reference" value="{{ old('reference', $order->reference) }}">
    @error('reference') <span>{{ $message }}</span> @enderror
</form>
  • Missing @csrf → 419; missing @method → 405
  • Compiled templates live in storage/framework/views; view:cache at deploy

Escaping rule of thumb: data in {{ }}, trusted markup in {!! !!}, and never trust a field you have not sanitized.

Continue Learning

Before You Start

Related Reading