LessonsBuild a Blog → Part 9

Search & Pagination

Laravel
⏱ 20 min read🏗️ ProjectNot completed

Let's add a powerful search feature and proper pagination to our blog so readers can easily find content.

Search Controller

app/Http/Controllers/SearchController.php
<?php
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class SearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $query = $request->get('q', '');

        $posts = collect(); // Empty collection by default

        if (strlen($query) >= 3) {
            $posts = Post::published()
                ->where(function($q) use ($query) {
                    $q->where('title', 'LIKE', "%{$query}%")
                      ->orWhere('body', 'LIKE', "%{$query}%")
                      ->orWhere('excerpt', 'LIKE', "%{$query}%")
                      ->orWhereHas('tags', fn($t) => $t->where('name', 'LIKE', "%{$query}%"))
                      ->orWhereHas('category', fn($c) => $c->where('name', 'LIKE', "%{$query}%"));
                })
                ->with(['user', 'category'])
                ->latest()
                ->paginate(10)
                ->withQueryString(); // Keep ?q= in pagination links
        }

        return view('blog.search', compact('posts', 'query'));
    }
}

Search Route

routes/web.php
<?php
Route::get('/search', SearchController::class)->name('blog.search');

Search View

resources/views/blog/search.blade.php
@extends('layouts.app')
@section('title', 'Search: ' . $query)

@section('content')
<div class="max-w-4xl mx-auto px-4 py-8">

    <!-- Search Box -->
    <form method="GET" action="{{ route('blog.search') }}" class="mb-8">
        <div class="flex gap-2">
            <input type="text"
                   name="q"
                   value="{{ $query }}"
                   placeholder="Search posts..."
                   class="flex-1 border rounded-lg px-4 py-3 text-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
            <button type="submit"
                    class="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700">
                Search
            </button>
        </div>
    </form>

    @if(strlen($query) < 3)
        <p class="text-gray-400">Please enter at least 3 characters to search.</p>
    @elseif($posts->isEmpty())
        <div class="text-center py-12">
            <div class="text-4xl mb-4">🔍</div>
            <h2 class="text-xl font-bold mb-2">No results found</h2>
            <p class="text-gray-400">No posts found for "{{ $query }}"</p>
        </div>
    @else
        <p class="text-gray-500 mb-6">
            {{ $posts->total() }} result(s) for "<strong>{{ $query }}</strong>"
        </p>

        @foreach($posts as $post)
        <article class="bg-white rounded-lg shadow p-6 mb-4 hover:shadow-md transition">
            <div class="flex justify-between items-start">
                <div>
                    @if($post->category)
                    <span class="text-xs font-semibold uppercase tracking-wide text-blue-600">
                        {{ $post->category->name }}
                    </span>
                    @endif
                    <h2 class="text-lg font-bold mt-1">
                        <a href="{{ route('blog.show', $post) }}"
                           class="hover:text-blue-600">
                            {!! str_ireplace($query, '<mark class="bg-yellow-100">' . $query . '</mark>', e($post->title)) !!}
                        </a>
                    </h2>
                    <p class="text-gray-500 mt-2 text-sm">{{ $post->excerpt }}</p>
                </div>
                @if($post->image)
                <img src="{{ Storage::url($post->image) }}"
                     class="w-24 h-16 object-cover rounded ml-4 flex-shrink-0" />
                @endif
            </div>
            <div class="flex items-center gap-4 mt-4 text-xs text-gray-400">
                <span>By {{ $post->user->name }}</span>
                <span>{{ $post->published_at->format('M d, Y') }}</span>
                <span>{{ $post->reading_time }}</span>
            </div>
        </article>
        @endforeach

        <div class="mt-6">{{ $posts->links() }}</div>
    @endif
</div>
@endsection

Search Bar in Layout

resources/views/layouts/app.blade.php (navbar)
<!-- Add to navbar -->
<form method="GET" action="{{ route('blog.search') }}" class="flex">
    <input type="text" name="q" placeholder="Search..."
           value="{{ request('q') }}"
           class="border rounded-l px-3 py-1 text-sm focus:outline-none" />
    <button type="submit"
            class="bg-blue-600 text-white px-3 py-1 rounded-r text-sm">🔍</button>
</form>

Custom Pagination Style

Terminal
# Publish pagination views to customize
php artisan vendor:publish --tag=laravel-pagination

# Files created in: resources/views/vendor/pagination/
# Edit tailwind.blade.php for Tailwind CSS styling
💡
Highlight search terms! The str_ireplace trick wraps the search term in a yellow <mark> tag in the results. This is a UX best practice used by Google and every major search engine.
💡
Always use withQueryString()! Without it, pagination links lose the ?q=laravel parameter — users get taken to page 2 of ALL posts instead of page 2 of search results.
← Previous Next Lesson →
💡

Stuck? Need help?

Review the previous lessons or check the Laravel documentation.

Laravel Docs →