What Are We Doing in This Post?
So far our controllers have been returning plain strings and JSON. Real web applications return HTML pages — structured, styled, with headers, footers, navigation, and dynamic content.
Blade is Laravel's templating engine. It lets you write HTML with special syntax for displaying variables, running loops, checking conditions, and most powerfully — building a master layout that every page inherits from.
By the end of this episode you will have a proper multi-page blog frontend with a shared layout, working navigation, and dynamic content passed from controllers.
What is a Templating Engine?
Without a templating engine, mixing PHP and HTML looks like this:
<?php
echo "<html><body>"; echo "<h1>" . $title . "</h1>"; foreach ($posts as $post) { echo "<div><h2>" . $post['title'] . "</h2>"; echo "<p>" . $post['body'] . "</p></div>"; } echo "</body></html>";
Messy, hard to read, and impossible to maintain at scale.
Blade gives you clean syntax that reads almost like plain HTML:
<html> <body> <h1>{{ $title }}</h1>
@foreach($posts as $post) <div> <h2>{{ $post['title'] }}</h2> <p>{{ $post['body'] }}</p> </div> @endforeach </body> </html>
Blade files have the extension .blade.php and live in resources/views/. Laravel compiles them into plain PHP behind the scenes — you never see the compiled output, you just write clean Blade syntax.
Displaying Variables — The {{ }} Syntax
The double curly braces display a variable's value:
<h1>{{ $title }}</h1> <p>Welcome, {{ $name }}!</p> <span>Total posts: {{ $count }}</span>
Importantly, {{ }} automatically runs htmlspecialchars() on the output. This means XSS attacks are prevented automatically — any HTML tags in user-supplied data get converted to safe entities before display.
If you intentionally want to output raw unescaped HTML, use {!! !!}:
{!! $htmlContent !!}
Only use {!! !!} when you are certain the content is safe — never on user-supplied input.
Blade Directives — Control Structures
Blade has clean directives for all PHP control structures. Every directive starts with @.
Conditionals:
@if($user->is_admin) <p>Welcome, Administrator.</p> @elseif($user->is_editor) <p>Welcome, Editor.</p> @else <p>Welcome, Guest.</p> @endif
Unless — opposite of if:
@unless($user->is_logged_in) <p>Please log in to continue.</p> @endunless
isset and empty checks:
@isset($post) <h1>{{ $post['title'] }}</h1> @endisset
@empty($posts) <p>No posts found.</p> @endempty
Loops:
@foreach($posts as $post) <div> <h2>{{ $post['title'] }}</h2> <p>{{ $post['body'] }}</p> </div> @endforeach
@for($i = 1; $i <= 5; $i++) <p>Item {{ $i }}</p> @endfor
@while($condition) <p>Looping...</p> @endwhile
forelse — foreach with an empty fallback:
@forelse($posts as $post) <div> <h2>{{ $post['title'] }}</h2> </div> @empty <p>No posts available yet.</p> @endforelse
@forelse is one of the most useful Blade directives. It loops through the collection and automatically shows the @empty block if the collection has no items. No separate @if(count($posts) > 0) check needed.
The $loop Variable
Inside any @foreach or @forelse, Blade gives you a special $loop variable with useful information about the current iteration:
@foreach($posts as $post) <div> <span>{{ $loop->iteration }}</span> <h2>{{ $post['title'] }}</h2>
@if($loop->first) <span>Latest Post</span> @endif
@if($loop->last) <span>End of Posts</span> @endif </div> @endforeach
$loop->iteration — current iteration number starting from 1.
$loop->index — current index starting from 0.
$loop->first — true on the first iteration.
$loop->last — true on the last iteration.
$loop->count — total number of items in the collection.
$loop->remaining — how many iterations are remaining.
Template Inheritance — The Most Powerful Blade Feature
This is the feature that makes Blade genuinely powerful.
Real world analogy: Think of a newspaper. Every page of the newspaper shares the same layout — the masthead at the top with the newspaper name, the footer with the date and page number, the column structure. The content on each page is different, but the surrounding layout is identical. If the newspaper redesigns its masthead, every page gets the new masthead automatically.
Blade template inheritance works exactly this way. You define one master layout file. Every page extends that layout and only provides the parts that change — the content. The shared layout wraps every page automatically.
Step 1 — Create the master layout:
Create resources/views/layouts/app.blade.php:
<!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@yield('title', 'My Blog')</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1e293b; }
nav { background: #1e293b; padding: 16px 32px; display: flex; justify-content: space-between; align-items: center; }
nav .brand { color: #fff; font-size: 20px; font-weight: 700; text-decoration: none; }
nav ul { list-style: none; display: flex; gap: 24px; }
nav ul a { color: #94a3b8; text-decoration: none; font-size: 14px; font-weight: 500; }
nav ul a:hover { color: #fff; }
.container { max-width: 860px; margin: 40px auto; padding: 0 20px; }
footer { text-align: center; padding: 32px; color: #64748b; font-size: 13px; border-top: 1px solid #e2e8f0; margin-top: 60px; } </style> @stack('styles') </head>
<body>
<nav> <a href="{{ route('home') }}" class="brand">MyBlog</a> <ul> <li><a href="{{ route('home') }}">Home</a></li> <li><a href="{{ route('posts.index') }}">Posts</a></li> </ul> </nav>
<div class="container"> @if (session('success')) <div style="background:#f0fdf4; border:1px solid #86efac; padding:12px 16px; border-radius:8px; margin-bottom:24px; color:#166534;"> {{ session('success') }} </div> @endif
@yield('content') </div>
<footer> © {{ date('Y') }} MyBlog. Built with Laravel. </footer>
@stack('scripts') </body>
</html>
Three important Blade directives in this layout:
@yield('title', 'My Blog') — a placeholder that child pages fill in. The second argument is the default value used when a child page does not define this section.
@yield('content') — the main content placeholder. Every child page provides its own content here.
@stack('styles') and @stack('scripts') — stacks that child pages can push CSS or JavaScript into. The layout renders them in the right place — styles in the head, scripts before the closing body tag.
Step 2 — Create Child Views That Extend the Layout
Create resources/views/home.blade.php:
@extends('layouts.app')
@section('title', 'Home — MyBlog')
@section('content') <div style="text-align:center; padding: 60px 0;"> <h1 style="font-size:36px; font-weight:800; margin-bottom:16px;"> Welcome to MyBlog </h1> <p style="font-size:17px; color:#64748b; margin-bottom:32px;"> A blog built with PHP and Laravel from scratch. </p> <a href="{{ route('posts.index') }}" style="background:#6366f1; color:#fff; padding:12px 28px; border-radius:8px; text-decoration:none; font-weight:600;"> Read Posts </a> </div> @endsection
@extends('layouts.app') — this page extends the master layout. Laravel wraps this page's content inside app.blade.php.
@section('title', 'Home — MyBlog') — fills the @yield('title') placeholder in the layout.
@section('content') ... @endsection — fills the @yield('content') placeholder with this page's actual content.
Create resources/views/posts/index.blade.php:
@extends('layouts.app')
@section('title', 'All Posts — MyBlog')
@push('styles') <style> .posts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 24px; }
.post-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 24px; }
.post-card h2 { font-size: 18px; font-weight: 700; margin-bottom: 10px; color: #1e293b; }
.post-card p { font-size: 14px; color: #64748b; line-height: 1.6; margin-bottom: 16px; }
.post-card .meta { font-size: 12px; color: #94a3b8; }
.post-card .badge { display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
.badge-published { background: #f0fdf4; color: #166534; }
.badge-draft { background: #fef9c3; color: #854d0e; }
.read-more { display: inline-block; margin-top: 12px; color: #6366f1; font-size: 13px; font-weight: 600; text-decoration: none; } </style> @endpush
@section('content') <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:32px;"> <h1 style="font-size:28px; font-weight:800;">All Posts</h1> <a href="{{ route('posts.create') }}" style="background:#6366f1; color:#fff; padding:10px 20px; border-radius:8px; text-decoration:none; font-size:14px; font-weight:600;">+ New Post</a> </div>
@forelse($posts as $post) <div class="posts-grid" style="display:block; margin-bottom:20px;"> <div class="post-card"> <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:10px;"> <h2>{{ $post['title'] }}</h2> <span class="badge badge-{{ $post['status'] }}"> {{ ucfirst($post['status']) }} </span> </div> <p>{{ Str::limit($post['body'], 100) }}</p> <div class="meta">By {{ $post['author'] }}</div> <a href="{{ route('posts.show', $post['id']) }}" class="read-more"> Read More → </a> </div> </div> @empty <div style="text-align:center; padding:60px; color:#64748b;"> <p style="font-size:18px;">No posts yet.</p> <a href="{{ route('posts.create') }}" style="color:#6366f1;"> Create your first post </a> </div> @endforelse @endsection
@push('styles') ... @endpush — pushes page-specific CSS into the @stack('styles') slot in the layout. The styles land exactly inside the <head> tag.
Str::limit($post['body'], 100) — a Laravel helper that truncates text to 100 characters and adds .... Clean one-liner instead of writing substr logic manually.
Create resources/views/posts/show.blade.php:
@extends('layouts.app')
@section('title', $post['title'] . ' — MyBlog')
@section('content') <article style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:40px; max-width:680px; margin:0 auto;">
<div style="margin-bottom:8px;"> <span style="background:#f0fdf4; color:#166534; padding:3px 10px; border-radius:20px; font-size:12px; font-weight:600;"> {{ ucfirst($post['status']) }} </span> </div>
<h1 style="font-size:30px; font-weight:800; margin:16px 0 8px;"> {{ $post['title'] }} </h1> <p style="color:#94a3b8; font-size:14px; margin-bottom:32px;"> By {{ $post['author'] }} </p>
<div style="font-size:16px; line-height:1.8; color:#334155;"> {{ $post['body'] }} </div>
<div style="margin-top:40px; padding-top:24px; border-top:1px solid #e2e8f0;"> <a href="{{ route('posts.index') }}" style="color:#6366f1; font-weight:600; text-decoration:none;"> ← Back to Posts </a> </div>
</article> @endsection
Create resources/views/posts/create.blade.php:
@extends('layouts.app')
@section('title', 'Create Post — MyBlog')
@section('content') <div style="background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:40px; max-width:600px; margin:0 auto;">
<h1 style="font-size:24px; font-weight:800; margin-bottom:28px;">Create New Post</h1>
<form method="POST" action="{{ route('posts.store') }}"> @csrf
<div style="margin-bottom:20px;"> <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Title</label> <input type="text" name="title" value="{{ old('title') }}" style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px;" required> </div>
<div style="margin-bottom:20px;"> <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Author</label> <input type="text" name="author" value="{{ old('author') }}" style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px;" required> </div>
<div style="margin-bottom:28px;"> <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px;">Body</label> <textarea name="body" rows="6" style="width:100%; padding:10px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px; resize:vertical;" required>{{ old('body') }}</textarea> </div>
<button type="submit" style="background:#6366f1; color:#fff; padding:12px 28px; border:none; border-radius:8px; font-size:15px; font-weight:600; cursor:pointer; width:100%;"> Publish Post </button> </form>
</div> @endsection
Two important things here.
@csrf — this directive generates a hidden input field with a security token. Laravel verifies this token on every POST, PUT, PATCH, and DELETE request to protect against Cross-Site Request Forgery attacks. Without @csrf, Laravel will reject your form submission with a 419 error. Always include it in every form.
{{ old('title') }} — if the form was submitted but failed validation, old() repopulates the field with what the user previously typed. They do not have to retype everything from scratch.
Update the Controller to Use These Views
Update app/Http/Controllers/PostController.php:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller { private $posts = [ [ 'id' => 1, 'title' => 'Getting Started With Laravel', 'body' => 'Laravel is a powerful PHP framework that makes web development enjoyable. It provides tools for routing, database management, authentication, and much more out of the box.', 'author' => 'Gagan', 'status' => 'published' ], [ 'id' => 2, 'title' => 'Understanding MVC Architecture', 'body' => 'MVC separates your application into Models, Views, and Controllers. This separation of concerns keeps your code organized, maintainable, and easy to scale.', 'author' => 'Rahul', 'status' => 'published' ], [ 'id' => 3, 'title' => 'Working With Eloquent ORM', 'body' => 'Eloquent makes database queries feel like writing plain English. Each database table has a corresponding Model class that you use to interact with that table.', 'author' => 'Priya', 'status' => 'draft' ], ];
public function index() { return view('posts.index', ['posts' => $this->posts]); }
public function create() { return view('posts.create'); }
public function store(Request $request) { return redirect()->route('posts.index')->with('success', 'Post created successfully!'); }
public function show($id) { $post = collect($this->posts)->firstWhere('id', (int) $id);
if (!$post) { abort(404, 'Post not found.'); }
return view('posts.show', ['post' => $post]); }
public function edit($id) { $post = collect($this->posts)->firstWhere('id', (int) $id);
if (!$post) { abort(404, 'Post not found.'); }
return view('posts.edit', ['post' => $post]); }
public function update(Request $request, $id) { return redirect()->route('posts.index')->with('success', 'Post updated successfully!'); }
public function destroy($id) { return redirect()->route('posts.index')->with('success', 'Post deleted successfully!'); } }
Update routes/web.php:
<?php
use Illuminate\Support\Facades\Route; use App\Http\Controllers\PostController;
Route::get('/', function () { return view('home'); })->name('home');
Route::resource('posts', PostController::class);
Now visit:
http://127.0.0.1:8000 — homepage with navigation.
http://127.0.0.1:8000/posts — all posts with cards.
http://127.0.0.1:8000/posts/1 — single post page.
http://127.0.0.1:8000/posts/create — create post form.
Every page shares the same navigation and footer from the layout. Change something in layouts/app.blade.php and it updates across every page instantly.
Blade Comments
Blade has its own comment syntax that does not appear in the rendered HTML output:
{{-- This is a Blade comment — not visible in browser source --}}
<!-- This is an HTML comment — visible in browser source -->
Use Blade comments for notes you never want users to see even when they view page source.
Conditional Classes — Clean Dynamic Styling
<span class="badge @if($post['status'] === 'published') badge-published @else badge-draft @endif"> {{ ucfirst($post['status']) }} </span>
Or using the cleaner @class directive:
<span @class([ 'badge', 'badge-published' => $post['status'] === 'published', 'badge-draft' => $post['status'] === 'draft', ])> {{ ucfirst($post['status']) }} </span>
@class takes an array where keys are class names and values are conditions. A class is only added when its condition is true.
What Did We Learn in This Post?
Blade is Laravel's templating engine. Files use the .blade.php extension and live in resources/views/.
{{ $variable }} displays data safely with XSS protection. {!! $html !!} outputs raw unescaped HTML — use with caution.
Blade directives — @if, @foreach, @forelse, @for, @while, @isset, @empty, @unless — provide clean syntax for all control structures.
@forelse with @empty handles both the loop and the empty state in one clean block.
$loop inside foreach gives useful iteration metadata — $loop->first, $loop->last, $loop->iteration, $loop->count.
Template inheritance with @extends, @section, @yield lets every page share a master layout. Change the layout once — every page updates.
@push and @stack inject page-specific CSS or JavaScript into the layout's head and footer from any child view.
@csrf is mandatory in every form — it generates a security token that protects against CSRF attacks.
old('fieldname') repopulates form fields after a failed submission.
What is Coming in Episode 19?
Our posts are currently hardcoded arrays in the controller. In Episode 19 we connect everything to a real database using Eloquent — Laravel's ORM.
We create a Post model, use it to fetch real records from the database, save new posts, update them, and delete them. The application becomes fully database-driven and everything we built in Episodes 16, 17, and 18 connects together into one working system.
See you in the next one.