Phase 2 — Module 2.1: Template Hierarchy — The Most Important WordPress Concept

What is Template Hierarchy?

When someone visits a URL on your WordPress site, WordPress has to decide which PHP file to load to render that page.

This decision-making system is called the Template Hierarchy.

WordPress looks for template files in a specific order — from most specific to most generic — and loads the first one it finds. If none are found, it always falls back to index.php.

Think of it like Laravel's route matching — most specific route wins. Same concept here, but with files instead of routes.


Why is This the Most Important Concept?

Because everything in theme development is built around this. When you create a template file with the right name — WordPress automatically uses it for the right content type. No routing code, no configuration — just the filename.

Visitor goes to /movies/inception/
        ↓
WordPress checks: what type of content is this?
        ↓
It is a single post of type 'movie'
        ↓
WordPress looks for template files in this order:
  1. single-movie-inception.php   ← most specific
  2. single-movie.php
  3. single.php
  4. singular.php
  5. index.php                    ← most generic (always exists)
        ↓
Loads the first file it finds

The Complete Template Hierarchy

Let us go through every content type and its template hierarchy.


1. Front Page (Homepage)

When someone visits https://streamvault.local/

front-page.php
      ↓
home.php
      ↓
index.php

Note: front-page.php is always used for the homepage regardless of whether you set a static page or latest posts in Settings → Reading. It is the highest priority homepage template.


2. Home / Blog Posts Index

When someone visits your blog posts page (the page you set in Settings → Reading → Posts page):

home.php
      ↓
index.php

3. Single Blog Post

When someone visits an individual blog post like /top-10-action-movies/

single-post-{slug}.php        e.g. single-post-top-10-action-movies.php
      ↓
single-post.php
      ↓
single.php
      ↓
singular.php
      ↓
index.php

4. Single Custom Post Type

When someone visits a single movie like /movies/inception/

single-{post-type}-{slug}.php    e.g. single-movie-inception.php
      ↓
single-{post-type}.php           e.g. single-movie.php
      ↓
single.php
      ↓
singular.php
      ↓
index.php

This is exactly what we will use for StreamVault. We will create single-movie.php and WordPress will automatically use it for every single movie page.


5. Static Page

When someone visits a page like /about/

{custom-template}.php            (if assigned via Page Attributes)
      ↓
page-{slug}.php                  e.g. page-about.php
      ↓
page-{id}.php                    e.g. page-5.php
      ↓
page.php
      ↓
singular.php
      ↓
index.php

So if you create a file called page-about.php in your theme — WordPress automatically uses it only for the About page. Every other page uses page.php.


6. Category Archive

When someone visits /category/movie-reviews/

category-{slug}.php              e.g. category-movie-reviews.php
      ↓
category-{id}.php                e.g. category-3.php
      ↓
category.php
      ↓
archive.php
      ↓
index.php

7. Tag Archive

When someone visits /tag/christopher-nolan/

tag-{slug}.php                   e.g. tag-christopher-nolan.php
      ↓
tag-{id}.php                     e.g. tag-8.php
      ↓
tag.php
      ↓
archive.php
      ↓
index.php

8. Custom Taxonomy Archive

When someone visits /genre/action/ (our StreamVault genre taxonomy)

taxonomy-{taxonomy}-{term}.php   e.g. taxonomy-genre-action.php
      ↓
taxonomy-{taxonomy}.php          e.g. taxonomy-genre.php
      ↓
taxonomy.php
      ↓
archive.php
      ↓
index.php

So taxonomy-genre.php will be used for ALL genre archive pages — action, drama, comedy, thriller, etc.


9. Custom Post Type Archive

When someone visits /movies/ (the movies archive page)

archive-{post-type}.php          e.g. archive-movie.php
      ↓
archive.php
      ↓
index.php

We will create archive-movie.php for the StreamVault movies listing page.


10. Author Archive

When someone visits /author/admin/

author-{nicename}.php            e.g. author-admin.php
      ↓
author-{id}.php                  e.g. author-1.php
      ↓
author.php
      ↓
archive.php
      ↓
index.php

11. Date Archive

When someone visits /2024/01/ or /2024/

date.php
      ↓
archive.php
      ↓
index.php

12. Search Results

When someone searches on your site — /search/inception/

search.php
      ↓
index.php

13. 404 Not Found

When someone visits a URL that does not exist

404.php
      ↓
index.php

14. Attachment Page

When someone visits a media file page

{mime-type}.php                  e.g. image.php, video.php
      ↓
attachment.php
      ↓
single.php
      ↓
singular.php
      ↓
index.php

Visual Map of the Entire Hierarchy

Any URL on your site
        │
        ├── Is it the homepage?
        │       ├── front-page.php
        │       └── home.php
        │
        ├── Is it a single post/CPT?
        │       ├── single-{type}-{slug}.php
        │       ├── single-{type}.php
        │       ├── single.php
        │       └── singular.php
        │
        ├── Is it a static page?
        │       ├── {custom-template}.php
        │       ├── page-{slug}.php
        │       ├── page-{id}.php
        │       ├── page.php
        │       └── singular.php
        │
        ├── Is it an archive?
        │       ├── Category → category-{slug}.php → category.php
        │       ├── Tag → tag-{slug}.php → tag.php
        │       ├── Taxonomy → taxonomy-{tax}-{term}.php → taxonomy-{tax}.php
        │       ├── CPT Archive → archive-{type}.php
        │       ├── Author → author-{name}.php → author.php
        │       ├── Date → date.php
        │       └── All fall back to → archive.php
        │
        ├── Is it a search results page?
        │       └── search.php
        │
        ├── Is it a 404 page?
        │       └── 404.php
        │
        └── ULTIMATE FALLBACK → index.php

How WordPress Finds These Files

WordPress looks for template files in this order:

1. Child theme folder             (if a child theme is active)
2. Parent theme folder            (your main theme folder)
3. Falls back to index.php        (always present)

This is why child themes work — you put only the files you want to override in the child theme, and WordPress finds them first.


Practical Example — StreamVault Templates We Will Build

Based on the template hierarchy, here are the files we will create for StreamVault:

streamvault/
├── index.php           ← ultimate fallback
├── front-page.php      ← StreamVault homepage (hero, featured movies)
├── single-movie.php    ← individual movie page
├── single-series.php   ← individual series page
├── archive-movie.php   ← all movies listing page
├── archive-series.php  ← all series listing page
├── taxonomy-genre.php  ← genre archive (action, drama, etc.)
├── taxonomy-language.php ← language archive
├── page.php            ← static pages (about, contact)
├── search.php          ← search results
├── 404.php             ← not found page
├── header.php          ← header partial
├── footer.php          ← footer partial
└── functions.php       ← theme brain

Every single filename is chosen based on the template hierarchy. WordPress will automatically route the right URL to the right file.


Custom Page Templates

Beyond the automatic hierarchy, you can create custom page templates — files that an admin can manually assign to any page from the Page Attributes panel.

To create a custom page template, add this comment at the top of any PHP file in your theme:


    <?php
    /*
    * Template Name: Full Width Page
    * Template Post Type: page
    */

Now in WordPress admin — when editing any Page — under Page Attributes → Template you will see "Full Width Page" as an option.

For StreamVault we will create:


    <?php
    /*
    * Template Name: StreamVault Home
    */
   

    <?php
    /*
    * Template Name: Movies Browse Page
    */


How to Debug Which Template is Loading

This is where Query Monitor plugin becomes useful. After activating it:

  1. Visit any page on your site frontend
  2. Click on Query Monitor bar at the top
  3. Click "Template" tab
Query Monitor → Template
------------------------
Template file:    single-movie.php
Theme:            streamvault

It tells you exactly which template file is being used for the current page. Extremely useful when debugging why a page looks wrong.

Alternatively — you can also temporarily add this to the top of index.php:

<?php echo get_template_part( basename( __FILE__, '.php' ) ); ?>

Or use this in any template to see its name:

<?php echo '<!-- Template: ' . basename(__FILE__) . ' -->'; ?>

Then inspect the page source — you will see the template name in an HTML comment.


The get_template_part() Function

Before finishing this module — one function you need to know right now.

get_template_part() lets you include reusable partial template files inside other templates:

get_template_part('template-parts/content', 'movie');

WordPress looks for these files in order:

template-parts/content-movie.php    ← first choice
template-parts/content.php          ← fallback

This is how we will build reusable components for StreamVault:

streamvault/
└── template-parts/
    ├── content-movie.php       ← movie card component
    ├── content-series.php      ← series card component
    ├── content-episode.php     ← episode item component
    └── content-none.php        ← "nothing found" message

Then in archive-movie.php:

while (have_posts()) {
    the_post();
    get_template_part('template-parts/content', 'movie');
}

Clean, reusable, exactly like Laravel's @include or components.


Summary

  • Template Hierarchy is WordPress's automatic routing system based on filenames.
  • WordPress always looks from most specific to most generic, falling back to index.php.
  • Single CPT: single-{type}.php — we will use single-movie.php and single-series.php.
  • CPT Archive: archive-{type}.php — we will use archive-movie.php.
  • Custom Taxonomy: taxonomy-{tax}.php — we will use taxonomy-genre.php.
  • Homepage: front-page.php takes highest priority.
  • Custom page templates are created with a Template Name: comment at the top.
  • get_template_part() is used for reusable partial files — like Laravel's @include.
  • Query Monitor → Template tab tells you exactly which file is loading for any page.


PHP & Laravel — Zero to Hero Episode 21: Authentication — User Registration, Login, and Route Protection

What Are We Doing in This Post?

Our blog works perfectly but it has a serious problem. Anyone who visits the site can create, edit, and delete any post. There is no concept of who is logged in or who owns what.

In this episode we build a complete authentication system from scratch — user registration, login, logout, session-based authentication, and route protection so only logged-in users can manage posts.

We are building this manually without any starter kit so you understand every single piece of how Laravel authentication works under the hood.


How Laravel Authentication Works

Laravel's authentication system is built on three things you already know from Core PHP.

Sessions — when a user logs in, their user ID is stored in the session. Every subsequent request, Laravel reads that session to know who the user is.

The Auth facade — a Laravel class that gives you clean methods like Auth::attempt(), Auth::login(), Auth::logout(), and Auth::user() to manage authentication state.

Middleware — the auth middleware protects routes by checking if a user is logged in before allowing access. If not logged in, the user gets redirected to the login page.


Step 1 — The User Model and Migration

Laravel already created a User model and migration when we installed it. The users table is already in your database from Episode 15.

Open app/Models/User.php and look at it — it already has $fillable with name, email, and password, and it already uses password hashing through Laravel's built-in features.

We do not need to change anything here. Laravel's User model is production-ready out of the box.


Step 2 — Create Authentication Controllers

We need two controllers — one for registration and one for login/logout.


    php artisan make:controller Auth/RegisterController
    php artisan make:controller Auth/LoginController

Laravel creates these inside app/Http/Controllers/Auth/ — a subfolder for auth-related controllers. Keeping them organized in a subfolder is standard practice.


Step 3 — Create Form Request Classes


    php artisan make:request Auth/RegisterRequest
    php artisan make:request Auth/LoginRequest

Update app/Http/Requests/Auth/RegisterRequest.php:


    <?php

    namespace App\Http\Requests\Auth;

    use Illuminate\Foundation\Http\FormRequest;

    class RegisterRequest extends FormRequest
    {
        public function authorize(): bool
        {
            return true;
        }

        public function rules(): array
        {
            return [
                'name'     => 'required|string|min:2|max:100',
                'email'    => 'required|email|unique:users,email',
                'password' => 'required|min:8|confirmed',
            ];
        }

        public function messages(): array
        {
            return [
                'name.required'      => 'Please enter your full name.',
                'name.min'           => 'Name must be at least 2 characters.',
                'email.required'     => 'Please enter your email address.',
                'email.email'        => 'Please enter a valid email address.',
                'email.unique'       => 'An account with this email already exists.',
                'password.required'  => 'Please choose a password.',
                'password.min'       => 'Password must be at least 8 characters.',
                'password.confirmed' => 'The passwords do not match.',
            ];
        }
    }

Update app/Http/Requests/Auth/LoginRequest.php:


    <?php

    namespace App\Http\Requests\Auth;

    use Illuminate\Foundation\Http\FormRequest;

    class LoginRequest extends FormRequest
    {
        public function authorize(): bool
        {
            return true;
        }

        public function rules(): array
        {
            return [
                'email'    => 'required|email',
                'password' => 'required',
            ];
        }

        public function messages(): array
        {
            return [
                'email.required'    => 'Please enter your email address.',
                'email.email'       => 'Please enter a valid email address.',
                'password.required' => 'Please enter your password.',
            ];
        }
    }


Step 4 — Build the Register Controller

Update app/Http/Controllers/Auth/RegisterController.php:


    <?php

    namespace App\Http\Controllers\Auth;

    use App\Http\Controllers\Controller;
    use App\Http\Requests\Auth\RegisterRequest;
    use App\Models\User;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Hash;

    class RegisterController extends Controller
    {
        public function create()
        {
            if (Auth::check()) {
                return redirect()->route('posts.index');
            }

            return view('auth.register');
        }

        public function store(RegisterRequest $request)
        {
            $user = User::create([
                'name'     => $request->input('name'),
                'email'    => $request->input('email'),
                'password' => Hash::make($request->input('password')),
            ]);

            Auth::login($user);

            return redirect()->route('posts.index')->with('success', 'Welcome to MyBlog, ' . $user->name . '!');
        }
    }

Auth::check() returns true if a user is already logged in. We redirect them away from the register page if they are already authenticated.

Hash::make() hashes the password using bcrypt before storing it in the database. Passwords are never stored as plain text. Ever.

Auth::login($user) logs the user in immediately after registration — no need to make them log in separately right after creating their account.


Step 5 — Build the Login Controller

Update app/Http/Controllers/Auth/LoginController.php:


    <?php

    namespace App\Http\Controllers\Auth;

    use App\Http\Controllers\Controller;
    use App\Http\Requests\Auth\LoginRequest;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;

    class LoginController extends Controller
    {
        public function create()
        {
            if (Auth::check()) {
                return redirect()->route('posts.index');
            }

            return view('auth.login');
        }

        public function store(LoginRequest $request)
        {
            $credentials = $request->only('email', 'password');
            $remember    = $request->boolean('remember');

            if (Auth::attempt($credentials, $remember)) {
                $request->session()->regenerate();
                return redirect()->intended(route('posts.index'))->with('success', 'Welcome back, ' . Auth::user()->name . '!');
            }

            return back()->withErrors([
                'email' => 'These credentials do not match our records.',
            ])->onlyInput('email');
        }

        public function destroy(Request $request)
        {
            Auth::logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();
            return redirect()->route('login')->with('success', 'You have been logged out successfully.');
        }
    }

Auth::attempt($credentials, $remember) checks the email and password against the database. It automatically hashes the provided password and compares it against the stored hash. If they match, it logs the user in and returns true. If not, it returns false.

The second argument $remember is a boolean — if true, Laravel creates a persistent "remember me" cookie so the user stays logged in even after closing the browser.

$request->session()->regenerate() generates a new session ID after login. This prevents session fixation attacks — a security vulnerability where an attacker could hijack a session.

redirect()->intended() redirects the user to wherever they were trying to go before being redirected to the login page. If they tried to visit /posts/create, got redirected to login, and then logged in — they land back on /posts/create. The second argument is the fallback URL if there was no intended destination.

withErrors() flashes specific error messages to the session. onlyInput('email') flashes only the email back to the form — never flash passwords back.

$request->session()->invalidate() and $request->session()->regenerateToken() on logout properly destroy the session and regenerate the CSRF token — a security requirement.


Step 6 — Update Routes

Update routes/web.php:


    <?php

    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\PostController;
    use App\Http\Controllers\Auth\RegisterController;
    use App\Http\Controllers\Auth\LoginController;

    Route::get('/', function () {
        return view('home');
    })->name('home');

    Route::middleware('guest')->group(function () {
        Route::get('/register', [RegisterController::class, 'create'])->name('register');
        Route::post('/register', [RegisterController::class, 'store']);
        Route::get('/login', [LoginController::class, 'create'])->name('login');
        Route::post('/login', [LoginController::class, 'store']);
    });

    Route::post('/logout', [LoginController::class, 'destroy'])->name('logout')->middleware('auth');

    Route::resource('posts', PostController::class)->only(['index', 'show']);

    Route::resource('posts', PostController::class)
        ->except(['index', 'show'])
        ->middleware('auth');

Two middleware groups used here.

middleware('guest') — the guest middleware redirects already-logged-in users away from the register and login pages. No point showing a login form to someone already logged in.

middleware('auth') — the auth middleware protects routes that require login. If an unauthenticated user tries to visit a protected route, they are redirected to the login page automatically.

We split the resource routes into two groups — index and show are public (anyone can read posts), while create, store, edit, update, and destroy require authentication.


Step 7 — Create Authentication Views

Create the folder resources/views/auth/.

Create resources/views/auth/register.blade.php:


    @extends('layouts.app')

    @section('title', 'Create Account — MyBlog')

    @section('content')

    <div style="max-width:480px; margin:0 auto;">

        <div style="text-align:center; margin-bottom:32px;">
            <h1 style="font-size:28px; font-weight:800; color:#1e293b;">Create your account</h1>
            <p style="color:#64748b; margin-top:8px;">Join MyBlog and start writing today.</p>
        </div>

        <div style="background:#fff; border:1px solid #e2e8f0; border-radius:16px; padding:36px;">

            @if($errors->any())
                <div style="background:#fef2f2; border:1px solid #fecaca; border-radius:8px; padding:16px; margin-bottom:24px;">
                    <p style="font-weight:700; color:#991b1b; margin-bottom:8px;">Please fix the following:</p>
                    <ul style="padding-left:20px; color:#b91c1c;">
                        @foreach($errors->all() as $error)
                            <li style="margin-bottom:4px; font-size:14px;">{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif

            <form method="POST" action="{{ route('register') }}">
                @csrf

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px; color:#374151;">Full Name</label>
                    <input
                        type="text"
                        name="name"
                        value="{{ old('name') }}"
                        placeholder="Your full name"
                        style="width:100%; padding:11px 14px; border:1px solid {{ $errors->has('name') ? '#ef4444' : '#e2e8f0' }}; border-radius:8px; font-size:15px; outline:none;"
                        autofocus
                    >
                    @error('name')
                        <p style="color:#ef4444; font-size:13px; margin-top:5px;">{{ $message }}</p>
                    @enderror
                </div>

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px; color:#374151;">Email Address</label>
                    <input
                        type="email"
                        name="email"
                        value="{{ old('email') }}"
                        placeholder="you@example.com"
                        style="width:100%; padding:11px 14px; border:1px solid {{ $errors->has('email') ? '#ef4444' : '#e2e8f0' }}; border-radius:8px; font-size:15px; outline:none;"
                    >
                    @error('email')
                        <p style="color:#ef4444; font-size:13px; margin-top:5px;">{{ $message }}</p>
                    @enderror
                </div>

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px; color:#374151;">Password</label>
                    <input
                        type="password"
                        name="password"
                        placeholder="Minimum 8 characters"
                        style="width:100%; padding:11px 14px; border:1px solid {{ $errors->has('password') ? '#ef4444' : '#e2e8f0' }}; border-radius:8px; font-size:15px; outline:none;"
                    >
                    @error('password')
                        <p style="color:#ef4444; font-size:13px; margin-top:5px;">{{ $message }}</p>
                    @enderror
                </div>

                <div style="margin-bottom:28px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px; color:#374151;">Confirm Password</label>
                    <input
                        type="password"
                        name="password_confirmation"
                        placeholder="Repeat your password"
                        style="width:100%; padding:11px 14px; border:1px solid #e2e8f0; border-radius:8px; font-size:15px; outline:none;"
                    >
                </div>

                <button type="submit" style="width:100%; background:#6366f1; color:#fff; padding:13px; border:none; border-radius:8px; font-size:15px; font-weight:700; cursor:pointer;">
                    Create Account
                </button>

            </form>

            <p style="text-align:center; margin-top:24px; font-size:14px; color:#64748b;">
                Already have an account?
                <a href="{{ route('login') }}" style="color:#6366f1; font-weight:600; text-decoration:none;">Sign in</a>
            </p>

        </div>
    </div>

    @endsection

Create resources/views/auth/login.blade.php:


    @extends('layouts.app')

    @section('title', 'Sign In — MyBlog')

    @section('content')

    <div style="max-width:480px; margin:0 auto;">

        <div style="text-align:center; margin-bottom:32px;">
            <h1 style="font-size:28px; font-weight:800; color:#1e293b;">Welcome back</h1>
            <p style="color:#64748b; margin-top:8px;">Sign in to manage your posts.</p>
        </div>

        <div style="background:#fff; border:1px solid #e2e8f0; border-radius:16px; padding:36px;">

            @if($errors->any())
                <div style="background:#fef2f2; border:1px solid #fecaca; border-radius:8px; padding:16px; margin-bottom:24px;">
                    <p style="font-weight:700; color:#991b1b; margin-bottom:8px;">Login failed:</p>
                    <ul style="padding-left:20px; color:#b91c1c;">
                        @foreach($errors->all() as $error)
                            <li style="margin-bottom:4px; font-size:14px;">{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
            @endif

            <form method="POST" action="{{ route('login') }}">
                @csrf

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px; color:#374151;">Email Address</label>
                    <input
                        type="email"
                        name="email"
                        value="{{ old('email') }}"
                        placeholder="you@example.com"
                        style="width:100%; padding:11px 14px; border:1px solid {{ $errors->has('email') ? '#ef4444' : '#e2e8f0' }}; border-radius:8px; font-size:15px; outline:none;"
                        autofocus
                    >
                    @error('email')
                        <p style="color:#ef4444; font-size:13px; margin-top:5px;">{{ $message }}</p>
                    @enderror
                </div>

                <div style="margin-bottom:20px;">
                    <label style="display:block; font-size:14px; font-weight:600; margin-bottom:6px; color:#374151;">Password</label>
                    <input
                        type="password"
                        name="password"
                        placeholder="Your password"
                        style="width:100%; padding:11px 14px; border:1px solid {{ $errors->has('password') ? '#ef4444' : '#e2e8f0' }}; border-radius:8px; font-size:15px; outline:none;"
                    >
                    @error('password')
                        <p style="color:#ef4444; font-size:13px; margin-top:5px;">{{ $message }}</p>
                    @enderror
                </div>

                <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:28px;">
                    <label style="display:flex; align-items:center; gap:8px; font-size:14px; color:#374151; cursor:pointer;">
                        <input type="checkbox" name="remember" style="width:16px; height:16px;">
                        Remember me
                    </label>
                </div>

                <button type="submit" style="width:100%; background:#6366f1; color:#fff; padding:13px; border:none; border-radius:8px; font-size:15px; font-weight:700; cursor:pointer;">
                    Sign In
                </button>

            </form>

            <p style="text-align:center; margin-top:24px; font-size:14px; color:#64748b;">
                Do not have an account?
                <a href="{{ route('register') }}" style="color:#6366f1; font-weight:600; text-decoration:none;">Create one</a>
            </p>

        </div>
    </div>

    @endsection


Step 8 — Update the Navigation Layout

Update resources/views/layouts/app.blade.php to show the correct navigation based on authentication state:


    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>@yield('title', 'MyBlog')</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;
                align-items: center;
            }

            nav ul a {
                color: #94a3b8;
                text-decoration: none;
                font-size: 14px;
                font-weight: 500;
            }

            nav ul a:hover {
                color: #fff;
            }

            .nav-btn {
                background: #6366f1;
                color: #fff !important;
                padding: 7px 16px;
                border-radius: 6px;
                font-size: 13px !important;
            }

            .nav-btn:hover {
                opacity: 0.88;
            }

            .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;
            }

            .alert-success {
                background: #f0fdf4;
                border: 1px solid #86efac;
                padding: 12px 16px;
                border-radius: 8px;
                margin-bottom: 24px;
                color: #166534;
                font-size: 14px;
            }
        </style>
        @stack('styles')
    </head>

    <body>

        <nav>
            <a href="{{ route('home') }}" class="brand">MyBlog</a>
            <ul>
                <li><a href="{{ route('posts.index') }}">Posts</a></li>

                @auth
                <li><a href="{{ route('posts.create') }}">Write Post</a></li>
                <li>
                    <span style="color:#64748b; font-size:13px;">{{ Auth::user()->name }}</span>
                </li>
                <li>
                    <form method="POST" action="{{ route('logout') }}" style="display:inline;">
                        @csrf
                        <button type="submit"
                            style="background:none; border:none; color:#94a3b8; font-size:14px; font-weight:500; cursor:pointer; padding:0;">
                            Logout
                        </button>
                    </form>
                </li>
                @else
                <li><a href="{{ route('login') }}">Sign In</a></li>
                <li><a href="{{ route('register') }}" class="nav-btn">Get Started</a></li>
                @endauth
            </ul>
        </nav>

        <div class="container">

            @if(session('success'))
            <div class="alert-success">{{ session('success') }}</div>
            @endif

            @yield('content')

        </div>

        <footer>
            &copy; {{ date('Y') }} MyBlog. Built with Laravel.
        </footer>

        @stack('scripts')
    </body>

    </html>

@auth ... @else ... @endauth is a Blade directive that renders its content only for authenticated users. The @else block renders for guests. This is the clean way to show different navigation to logged-in vs logged-out users.

Auth::user() returns the currently authenticated User model. Auth::user()->name gives you their name.


Step 9 — Protect Posts With Ownership

Right now any logged-in user can edit or delete any post. Let us add ownership — users can only manage their own posts.

First update the posts migration to make user_id required. Run:


    php artisan migrate:fresh --seed

Update PostController to associate posts with the logged-in user and enforce ownership:


    <?php

    namespace App\Http\Controllers;

    use App\Http\Requests\StorePostRequest;
    use App\Http\Requests\UpdatePostRequest;
    use App\Models\Post;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Str;

    class PostController extends Controller
    {
        public function index()
        {
            $posts = Post::latest()->get();
            return view('posts.index', ['posts' => $posts]);
        }

        public function create()
        {
            return view('posts.create');
        }

        public function store(StorePostRequest $request)
        {
            Post::create([
                'user_id'      => Auth::id(),
                'title'        => $request->input('title'),
                'slug'         => Str::slug($request->input('title')),
                'body'         => $request->input('body'),
                'status'       => $request->input('status'),
                'published_at' => $request->input('status') === 'published' ? now() : null,
            ]);

            return redirect()->route('posts.index')->with('success', 'Post created successfully!');
        }

        public function show($id)
        {
            $post = Post::findOrFail($id);
            $post->increment('views');
            return view('posts.show', ['post' => $post]);
        }

        public function edit($id)
        {
            $post = Post::findOrFail($id);

            if ($post->user_id !== Auth::id()) {
                abort(403, 'You do not have permission to edit this post.');
            }

            return view('posts.edit', ['post' => $post]);
        }

        public function update(UpdatePostRequest $request, $id)
        {
            $post = Post::findOrFail($id);

            if ($post->user_id !== Auth::id()) {
                abort(403, 'You do not have permission to update this post.');
            }

            $post->update([
                'title' => $request->input('title'),
                'slug'  => Str::slug($request->input('title')),
                'body'  => $request->input('body'),
            ]);

            return redirect()->route('posts.show', $post->id)->with('success', 'Post updated successfully!');
        }

        public function destroy($id)
        {
            $post = Post::findOrFail($id);

            if ($post->user_id !== Auth::id()) {
                abort(403, 'You do not have permission to delete this post.');
            }

            $post->delete();
            return redirect()->route('posts.index')->with('success', 'Post deleted successfully!');
        }
    }

Auth::id() returns the ID of the currently logged-in user — a shortcut for Auth::user()->id.

abort(403) returns a Forbidden response. Laravel has a beautiful default 403 error page. 403 means the user is authenticated but not authorized to perform this action.


Update the Show View to Conditionally Show Edit/Delete

Update resources/views/posts/show.blade.php — only show edit and delete buttons to the post owner:


    @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:700px; margin:0 auto;">

        <span
            style="display:inline-block; padding:3px 10px; border-radius:20px; font-size:11px; font-weight:600; background:#f0fdf4; color:#166534; margin-bottom:16px;">
            {{ ucfirst($post->status) }}
        </span>

        <h1 style="font-size:30px; font-weight:800; margin-bottom:8px; color:#1e293b;">{{ $post->title }}</h1>

        <div
            style="display:flex; gap:16px; color:#94a3b8; font-size:13px; margin-bottom:32px; padding-bottom:24px; border-bottom:1px solid #e2e8f0;">
            <span>{{ $post->created_at->format('d M Y') }}</span>
            <span>{{ $post->views }} views</span>
        </div>

        <div style="font-size:16px; line-height:1.85; color:#334155;">
            {{ $post->body }}
        </div>

        <div
            style="margin-top:40px; padding-top:24px; border-top:1px solid #e2e8f0; display:flex; gap:16px; align-items:center;">
            <a href="{{ route('posts.index') }}" style="color:#6366f1; font-weight:600; text-decoration:none;">← All
                Posts</a>

            @auth
            @if(Auth::id() === $post->user_id)
            <a href="{{ route('posts.edit', $post->id) }}"
                style="color:#f59e0b; font-weight:600; text-decoration:none;">Edit</a>

            <form method="POST" action="{{ route('posts.destroy', $post->id) }}" style="display:inline;"
                onsubmit="return confirm('Delete this post?')">
                @csrf
                @method('DELETE')
                <button type="submit"
                    style="background:none; border:none; color:#ef4444; font-weight:600; cursor:pointer; font-size:16px; padding:0;">Delete</button>
            </form>
            @endif
            @endauth
        </div>

    </article>

    @endsection


Testing the Complete Auth Flow

Start the server:

php artisan serve

Visit http://127.0.0.1:8000/register — register a new account. You are logged in automatically and redirected to posts.

The navigation now shows your name, a Write Post link, and a Logout button.

Visit http://127.0.0.1:8000/posts/create while logged out — you get redirected to login automatically. That is the auth middleware working.

Login and create a post. Go to the post — you see Edit and Delete buttons because you are the owner.

Open a different browser or incognito window — visit the same post as a guest. No Edit or Delete buttons visible.

Try visiting /posts/1/edit directly as a different user — you get a 403 Forbidden error.

This is complete authentication and authorization working together.


What Did We Learn in This Post?

Laravel authentication is built on sessions, the Auth facade, and middleware. No magic — the same concepts from Core PHP, just implemented cleanly by the framework.

Auth::attempt() verifies credentials and logs the user in. Auth::login() logs a user in directly. Auth::logout() destroys the session. Auth::check() returns true if logged in. Auth::user() returns the logged-in User model. Auth::id() returns their ID.

Hash::make() bcrypt-hashes passwords before storing. Passwords are never stored as plain text.

session()->regenerate() after login prevents session fixation attacks. session()->invalidate() and session()->regenerateToken() after logout properly destroy the session.

middleware('auth') protects routes — unauthenticated users get redirected to login. middleware('guest') protects auth pages — authenticated users get redirected away.

redirect()->intended() sends users back to where they were trying to go before being redirected to login.

@auth ... @else ... @endauth in Blade renders different content for authenticated vs guest users.

abort(403) returns a Forbidden response when a user is authenticated but not authorized for an action.


What is Coming in Episode 22?

Our blog now has users and posts. But there is no relationship defined between them in Eloquent. Episode 22 covers Eloquent Relationships — belongsTo, hasMany, hasOne, and belongsToMany. We define the User-Post relationship properly and use it to display the author's name on each post, fetch all posts by a specific user, and understand how Laravel handles related data elegantly.

See you in the next one.


Next Episode: Eloquent Relationships — Connecting Models Together

This is Episode 21 of the PHP and Laravel — Zero to Hero series.


PHP & Laravel — Zero to Hero Episode 23: Deploying Laravel — Taking Your Application Live

What Are We Doing in This Post? Your Laravel blog is complete. It has authentication, posts, relationships, validation, and a clean UI. But...