Blade Templates
Layouts, components, slots, directives and escaping.
Blade is Laravel's templating engine. It looks like HTML with a few extras: {{ }} for safe output, @directives for control flow, and an HTML-like syntax for reusable components. Under the hood every Blade file is compiled once into plain PHP and cached, so there's no runtime cost compared with writing PHP templates by hand.
Blade code captioned with a file path is copied from this site's own views: the CMS pages and these tutorial pages. The Projects & Tasks example in this app is a JSON API, so its view layer is API resources (ProjectResource, TaskResource), not Blade. Snippets that render projects and tasks are therefore marked Illustrative. They use the example's real models and methods ($task->isComplete(), $task->priority->label(), $project->tasks) to show how you'd render the same data server-side.
How Blade compiles
When you call view('projects.index'), Laravel finds resources/views/projects/index.blade.php. It checks whether a compiled copy in storage/framework/views is up to date, recompiles if needed, then includes the compiled PHP with your data extracted into local variables.
php artisan view:cache precompiles every template at deploy time, and view:clear empties the cache.
Output and escaping
Illustrative:
<h1>{{ $project->name }}</h1> {{-- escaped with htmlspecialchars() --}}
<div>{!! $page->rendered_html !!}</div> {{-- raw, unescaped --}}
<p>{{ $task->due_on?->format('d M') ?? 'No due date' }}</p>
@{{ thisIsLeftForVue }} {{-- @ prints the braces literally --}}
{{ }} runs your value through e(), which converts <, >, &, " and ' to HTML entities. That single default stops most XSS attacks. {!! !!} skips it, so only use it for HTML you generated or sanitised yourself.
Try it. Type something malicious and compare the two outputs:
This site uses {!! !!} in exactly two trusted places: the CMS text block (HTML from the admin's rich-text editor) and these tutorial pages (Markdown from the Git repo, rendered by TutorialRepository). Never use it for anything a visitor typed.
Control-flow directives
Illustrative. These render a project from the example. open_tasks_count is the same withCount alias the API controller uses:
@if ($project->tasks->isEmpty())
<p>No tasks yet.</p>
@elseif ($project->open_tasks_count === 0)
<p>🎉 All done!</p>
@else
<p>{{ $project->open_tasks_count }} of {{ $project->tasks_count }} tasks open</p>
@endif
@unless ($task->isComplete()) <button>Mark complete</button> @endunless
@isset($task->due_on) <p>Due {{ $task->due_on->diffForHumans() }}</p> @endisset
@auth <a href="/logout">Log out</a> @endauth
@guest <a href="/login">Log in</a> @endguest
@can('update', $task) {{-- asks the real App\Policies\TaskPolicy --}}
<a href="{{ route('tasks.edit', $task) }}">Edit</a>
@endcan
@env('local') <div class="debug-bar">LOCAL</div> @endenv
Loops and the $loop variable
Illustrative:
<ul>
@forelse ($project->tasks as $task)
<li @class(['task', 'task--done' => $task->isComplete(), 'task--first' => $loop->first])>
{{ $loop->iteration }}/{{ $loop->count }}: {{ $task->title }} ({{ $task->priority->label() }})
</li>
@empty
<li>No tasks yet.</li>
@endforelse
</ul>
Inside any loop, $loop gives you index, iteration, first, last, even, odd, remaining, depth and parent (for nested loops). The CMS homepage uses $loop->last to drop the bottom margin on the final blog-type card, and $loop->iteration to number the levels on each tutorial track page:
resources/views/blocks/blog_types.blade.php
style="max-width:1060px; margin:0 auto {{ $loop->last ? '0' : '2rem' }}; …"
resources/views/tutorials/track.blade.php
<span class="tut-level-badge">Level {{ $loop->iteration }}</span>
Attribute helpers
Illustrative:
<input type="checkbox" name="completed" @checked($task->isComplete())>
<select name="priority">
@foreach (App\Enums\TaskPriority::cases() as $priority)
<option value="{{ $priority->value }}" @selected($task->priority === $priority)>{{ $priority->label() }}</option>
@endforeach
</select>
<button @disabled($task->isComplete())>Mark complete</button>
<div @class(['card', 'card--high' => $task->priority === App\Enums\TaskPriority::High]) @style(['opacity: .5' => $task->isComplete()])></div>
Layouts: two approaches
1. Template inheritance (@extends / @yield)
This is the classic approach, and it's what this site's views use. The layout defines holes, and child views fill them:
resources/views/layouts/app.blade.php (trimmed)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
<style>/* … site styles, including .prose for long-form content … */</style>
@stack('head')
</head>
<body>
<header class="site-header" id="site-header">…nav…</header>
<main>
@yield('content')
</main>
<footer class="site-footer">…</footer>
@stack('scripts')
</body>
</html>
resources/views/tutorials/show.blade.php (trimmed): the template of this page
@extends('layouts.app')
@section('title', $page['title'] . ' · ' . $track['title'])
@include('tutorials._styles')
@push('head')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
@endpush
@section('content')
<div class="tut-lesson" style="--accent: {{ $track['accent'] }};">
{{-- sidebar · article (rendered Markdown) · table of contents --}}
</div>
@endsection
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
{{-- … --}}
@endpush
@push/@stack let a child (or any partial or component) add CSS or JS to the layout's <head> or footer. The lesson page you're reading pushes the highlight.js stylesheet, and the Vue runtime for demos, this way.
2. Component layouts (<x-layout>)
Illustrative, not in this app. This is the modern approach, used by Laravel's starter kits. The layout is a component, and the page content is its slot:
{{-- resources/views/components/layout.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head><title>{{ $title ?? config('app.name') }}</title></head>
<body>
<main>{{ $slot }}</main>
</body>
</html>
{{-- resources/views/projects/index.blade.php --}}
<x-layout title="Projects">
<h1>Projects</h1>
…
</x-layout>
@extends + @section |
<x-layout> component |
|
|---|---|---|
| Mental model | Child fills parent's holes | Parent wraps child |
| Passing data | @section('title', 'X') |
Attributes / named slots |
| Nesting layouts | Chains of @extends |
Components inside components |
| Typical in | Older apps, CMS themes (this site) | The official React, Vue and Livewire starter kits, new apps |
Both are fully supported. Pick one per project and stay consistent.
Partials: @include
Illustrative:
@include('projects._task-row', ['task' => $task])
@includeWhen($project->open_tasks_count === 0, 'projects._all-done-banner')
@each('projects._task-row', $project->tasks, 'task', 'projects._no-tasks')
This site's CMS renders each page as a list of blocks stored as JSON. The page view picks a partial per block type dynamically:
resources/views/pages/show.blade.php
@extends('layouts.app')
@section('title', $page->title)
@section('content')
@forelse ($page->blocks ?? [] as $block)
@php $type = $block['type'] ?? null; $data = $block['data'] ?? []; @endphp
@if ($type && View::exists("blocks.{$type}"))
@include("blocks.{$type}", $data)
@else
{{-- Unknown block type — silently skip --}}
@endif
@empty
<div style="text-align:center; padding:4rem 2rem; color:#9ca3af;">
<p>This page has no blocks yet. <a href="/admin" style="color:#6366f1;">Add some in the admin.</a></p>
</div>
@endforelse
@endsection
Adding a new block type means dropping a new file in resources/views/blocks/. There's no controller change at all.
Components
Illustrative, not in this app. This site doesn't define any Blade components of its own (the Filament admin panel uses plenty). The examples below build a priority badge and a task card for the Projects & Tasks models.
Components are reusable UI pieces with their own props and slots. Anonymous components are just a Blade file:
{{-- resources/views/components/badge.blade.php --}}
@props(['color' => 'gray'])
<span {{ $attributes->class(["badge badge--{$color}"]) }}>
{{ $slot }}
</span>
<x-badge color="green">Done</x-badge>
<x-badge :color="$task->priority === App\Enums\TaskPriority::High ? 'red' : 'gray'" class="ml-2">
{{ $task->priority->label() }}
</x-badge>
$attributes holds every attribute that isn't a declared prop. ->class() and ->merge() combine them with defaults, so callers can add classes without the component having to plan for them.
Class-based components
When a component needs logic or injected services, generate a class:
php artisan make:component TaskCard
namespace App\View\Components;
class TaskCard extends Component
{
public function __construct(
public Task $task,
public bool $compact = false,
) {}
public function isOverdue(): bool
{
return ! $this->task->isComplete() && $this->task->due_on?->isPast();
}
public function render(): View
{
return view('components.task-card');
}
}
{{-- resources/views/components/task-card.blade.php --}}
<article {{ $attributes->class(['task-card', 'task-card--overdue' => $isOverdue()]) }}>
<h3>{{ $task->title }}</h3>
@unless ($compact)
<p>{{ $task->description }}</p>
@endunless
@isset($actions)
<footer>{{ $actions }}</footer>
@endisset
</article>
<x-task-card :task="$task" compact>
<x-slot:actions>
<a href="{{ route('tasks.edit', $task) }}">Edit</a>
</x-slot:actions>
</x-task-card>
Prefix an attribute with : to pass a PHP expression (:task="$task"). Without the colon you get a plain string.
Blade or Vue? Blade components and Vue components solve the same problem (reusable, prop-driven UI) on opposite sides of the wire. Blade renders on the server, once per request. Vue renders and re-renders in the browser. Many Laravel apps use Blade for the page shell and SEO-friendly content, and mount Vue only where you need rich interactivity. These tutorial pages do exactly that.
Forms
Illustrative, not in this app. This is an HTML form for editing a task. Its fields match the real StoreTaskRequest rules (title 3–120 characters, priority must be a TaskPriority value), which the Forms & Validation lesson covers. That lesson also shows a live demo against the real POST /api/demo/tasks/validate endpoint.
<form method="POST" action="{{ route('tasks.update', $task) }}">
@csrf
@method('PUT')
<label for="title">Title</label>
<input id="title" name="title" value="{{ old('title', $task->title) }}" @class(['is-invalid' => $errors->has('title')])>
@error('title')
<p class="error">{{ $message }}</p>
@enderror
<label for="priority">Priority</label>
<select id="priority" name="priority">
@foreach (App\Enums\TaskPriority::cases() as $priority)
<option value="{{ $priority->value }}" @selected(old('priority', $task->priority->value) === $priority->value)>{{ $priority->label() }}</option>
@endforeach
</select>
<button type="submit">Save</button>
</form>
@csrfadds the hidden token that thewebmiddleware group checks. Without it you get a 419.@method('PUT')spoofs the verb for resource routes.old('title', $default)refills the field after a failed validation redirect.@errorand$errorsread the validation messages flashed to the session.
Sharing data with every view
There are three ways, from most to least targeted.
1. Pass it from the controller. This is how TutorialController::show() hands this page its data:
app/Http/Controllers/TutorialController.php
return response(view('tutorials.show', [
'trackKey' => $track,
'track' => $trackData,
// ...
'content' => $this->tutorials->render($track, $slug),
]));
2. A view composer runs whenever matching views render. This site uses a '*' composer to give every view $siteConfig (title, primary colour), so the layout can theme itself per deployed site:
app/Providers/AppServiceProvider.php
View::composer('*', function (\Illuminate\View\View $view) {
$config = app(SiteConfigService::class)->get();
$view->with('siteConfig', $config);
});
3. A global share is available everywhere. Illustrative:
View::share('priorities', App\Enums\TaskPriority::cases());
Key takeaways
- Blade compiles to cached plain PHP, so it adds no runtime overhead.
{{ }}escapes output (XSS-safe by default). Reserve{!! !!}for trusted HTML.- Use
@extends/@yieldor<x-layout>for layouts, and@push/@stackfor per-page assets. - Components (
<x-...>) give you props,$attributesmerging and named slots, and can be anonymous or class-based. - Forms need
@csrf, spoofed methods forPUT/DELETE, andold()+@errorfor validation feedback.