SOLARISE
DEV

Build a Reactive Laravel Blog with Livewire: A Step-by-Step Tutorial

Blazing-fast updates, minimal JavaScript

Originally published: September 14th, 2023. Updated on: April 29th, 2025.

In this tutorial, we’ll build a basic Laravel blog application using Livewire to achieve seriously snappy front-end responsiveness without writing complex JavaScript[cite: 1225].

Laravel & Livewire Make a Powerful Team

As a freelance web developer, I frequently use Laravel for projects[cite: 1226]. It's an awesome PHP framework capable of building a wide range of applications[cite: 1227]. Livewire makes it even more powerful[cite: 1227, 1228].

Livewire is a full-stack framework for Laravel that simplifies creating dynamic, reactive front-end interfaces[cite: 1228]. Normally, achieving highly interactive UIs requires JavaScript frameworks like React, Vue, or AlpineJS[cite: 1229]. Livewire, however, lets you build these experiences using familiar Laravel syntax (PHP and Blade templates) without stepping outside the core framework[cite: 1230, 1233].

Created by Caleb Porzio, Livewire has gained significant traction[cite: 1232]. Its coolest feature? Reflecting server state changes on the front-end without a full page reload[cite: 1234]. This enables near-instant responses to user interactions[cite: 1235].

We'll use this capability to add two key responsive features to our blog:

  1. Pagination: Clicking through article pages without reloading the entire view[cite: 1236].
  2. Navigation: Moving between the article index and individual posts seamlessly, again without full page reloads[cite: 1236].

Setting Up a Simple Laravel Blog with Livewire

Let's dive in! We'll build a basic blog showing an index of articles and a view for single articles[cite: 1236].

Our simple blog application will feature:

  • A basic Article model and database structure[cite: 1246].
  • Dummy blog content with titles, text, and featured images[cite: 1246].
  • Routing for the article index and single post pages[cite: 1237].
  • Livewire integration for dynamic interactions[cite: 1237].
  • Pagination for Browse articles[cite: 1237].
  • Super-fast navigation using Livewire's wire:navigate[cite: 1237].

Prerequisites:

  • A suitable local development environment (PHP, Composer)[cite: 1238]. Using WSL on Windows can simplify things[cite: 1239].
  • Composer installed[cite: 1239].
  • A code editor (VS Code is a great choice)[cite: 1239].
  • Basic command-line familiarity[cite: 1021].
  • We'll use php artisan serve for the local server, no complex build setup needed[cite: 1239].

Let's Get Going

  1. Create a New Laravel Project: If you don't have one, run:

    composer create-project --prefer-dist laravel/laravel your-project-name
    

    (This might take a moment)[cite: 1239]. If you encounter PHP version errors, ensure you have the correct version and required extensions (like pdo_sqlite) installed[cite: 1241]. Then, navigate into the project directory:

    cd your-project-name
    

    [cite: 1241]

  2. Install Livewire:

    composer require livewire/livewire
    

    [cite: 1241] Optionally, publish the config file (nice for review/updates, but not essential):

    php artisan livewire:publish --config
    

    This copies settings to config/livewire.php[cite: 1242].

  3. Set Up the Database (SQLite): SQLite is a simple file-based database, great for local development[cite: 1243, 1244].

    • Make sure SQLite3 is installed (e.g., sudo apt-get install sqlite3 on Debian/Ubuntu)[cite: 1244].
    • Create the database file:
      touch database/database.sqlite
      
      [cite: 1245]
    • Update your .env file:
      DB_CONNECTION=sqlite
      # DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD can usually be removed or commented out for SQLite
      DB_DATABASE=/absolute/path/to/your-project-name/database/database.sqlite
      
      (Make sure to use the correct absolute path to your database.sqlite file)[cite: 1245].
  4. Run Your App: Start the development server:

    php artisan serve
    

    [cite: 1245] You should now be able to view the default Laravel welcome page in your browser (usually at http://127.0.0.1:8000)[cite: 1245].

Setting Up the Laravel Blog & Articles Structure

We need a way to store and manage our blog articles.

  1. Create the Migration:

    php artisan make:migration create_articles_table
    

    [cite: 1247] This creates a migration file in database/migrations. Open it and modify the up() method:

    // database/migrations/xxxx_xx_xx_xxxxxx_create_articles_table.php
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    return new class extends Migration
    {
        public function up(): void
        {
            Schema::create('articles', function (Blueprint $table) {
                $table->id(); // Auto-incremental primary key
                $table->string('title'); // Title of the article
                $table->text('content'); // Content of the article
                $table->string('image')->nullable(); // Adding the image column
                $table->timestamps(); // created_at and updated_at timestamps
            });
        }
    
        public function down(): void
        {
            Schema::dropIfExists('articles');
        }
    };
    

    [cite: 1248]

  2. Run the Migration:

    php artisan migrate
    

    [cite: 1249] This creates the articles table in your SQLite database[cite: 1249].

  3. Create the Model and Controller:

    php artisan make:model Article -c -m
    

    This command is slightly different from the source (--controller) but achieves the same goal by creating the Model (Article.php in app/Models), the Controller (ArticleController.php in app/Http/Controllers), and references the migration we just created (-m)[cite: 1260]. The -c flag creates the controller.

  4. Populate Dummy Data (Seeding): Let's add some sample articles.

    • Create a Seeder:
      php artisan make:seeder ArticlesTableSeeder
      
      [cite: 1251]
    • Install Faker if you don't have it (often included with Laravel dev dependencies):
      composer require fakerphp/faker --dev
      
      (Note: The original source used fzaninotto/faker which is abandoned. fakerphp/faker is the community replacement.) [cite: 1254]
    • Edit database/seeders/ArticlesTableSeeder.php:
      <?php
      
      namespace Database\Seeders;
      
      use Illuminate\Database\Console\Seeds\WithoutModelEvents;
      use Illuminate\Database\Seeder;
      use Illuminate\Support\Facades\DB; // Import DB facade
      use Faker\Factory as Faker; // Import Faker
      
      class ArticlesTableSeeder extends Seeder
      {
          /**
           * Run the database seeds.
           */
          public function run(): void
          {
              $faker = Faker::create(); [cite: 1252]
      
              foreach (range(1, 10) as $index) { // Create 10 articles
                  DB::table('articles')->insert([
                      'title' => $faker->sentence(6, true),
                      'content' => $faker->paragraphs(3, true),
                      'image' => "[https://placekitten.com/500/500?image=](https://placekitten.com/500/500?image=)" . $index, // Use unique kitten images
                      'created_at' => now(),
                      'updated_at' => now(),
                  ]);
              } [cite: 1253, 1254]
          }
      }
      
    • Call the seeder from database/seeders/DatabaseSeeder.php:
      <?php
      
      namespace Database\Seeders;
      
      use Illuminate\Database\Seeder;
      
      class DatabaseSeeder extends Seeder
      {
          /**
           * Seed the application's database.
           */
          public function run(): void
          {
              $this->call([
                  ArticlesTableSeeder::class,
                  // Add other seeders here if needed
              ]);
          }
      }
      
    • Run the seeder:
      php artisan db:seed --class=ArticlesTableSeeder
      
      [cite: 1256] Or to refresh the database and seed:
      php artisan migrate:fresh --seed
      
      (Using --seed with migrate:fresh runs the main DatabaseSeeder, which now calls our ArticlesTableSeeder) [cite: 1256]

Setting up the Views and Routing (Initial Non-Livewire Setup)

Before integrating Livewire, let's set up the basic Laravel views and routes.

  1. Update Article Controller (app/Http/Controllers/ArticleController.php): Add methods to show the index and a single article.

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Article; // Import the Article model
    use Illuminate\Http\Request;
    
    class ArticleController extends Controller
    {
        /**
         * Display a listing of the resource.
         */
        public function index()
        {
            $articles = Article::latest()->get(); // Get latest articles
            // We'll replace this view later with Livewire
            return view('articles.index', compact('articles')); [cite: 1263]
        }
    
        /**
         * Display the specified resource.
         */
        public function show(Article $article) // Use route model binding
        {
             // We'll replace this view later with Livewire
            return view('articles.show', compact('article'));
        }
    
        // Add other methods (create, store, edit, update, destroy) later if needed
    }
    
    

    Make sure use App\Models\Article; is included at the top[cite: 1265].

  2. Create View Files: If they don't exist, create these directories and files:

    • resources/views/layouts/app.blade.php
    • resources/views/articles/index.blade.php
    • resources/views/articles/show.blade.php You can use php artisan make:view articles.index etc., if preferred[cite: 1267, 1271].
  3. Create Base Layout (resources/views/layouts/app.blade.php): This is our main HTML structure. We'll include Tailwind via CDN for simplicity[cite: 1276, 1277].

    <!DOCTYPE html>
    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Laravel Livewire Blog</title>
        <script src="[https://cdn.tailwindcss.com?plugins=typography](https://cdn.tailwindcss.com?plugins=typography)"></script>
        @livewireStyles </head>
    <body class="bg-gray-100 text-gray-800 font-sans">
    
        <nav class="bg-blue-600 text-white p-4">
            <div class="container mx-auto flex justify-between items-center">
                 <a href="/articles" class="text-2xl font-bold hover:text-blue-200">Laravel Livewire Blog</a> </div>
        </nav>
    
        <main class="container mx-auto mt-8 p-4">
            @yield('content') </main>
    
        <footer class="bg-gray-700 text-white p-4 mt-8">
            <div class="container mx-auto text-center">
                © {{ date('Y') }} Laravel Livewire Blog. All rights reserved. </div>
        </footer>
    
        @livewireScripts </body>
    </html>
    

    (Note: Added @livewireStyles and @livewireScripts here already, as per later steps in the source)[cite: 1295].

  4. Create Initial Index View (resources/views/articles/index.blade.php): This will display the list of articles (we'll replace its core content with a Livewire component soon).

    @extends('layouts.app') {{-- Use the main layout --}}
    
    @section('content')
    
    <div class="container mx-auto">
        <h2 class='my-4 text-3xl font-bold'>Articles</h2>
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {{-- We will replace this loop with a Livewire component --}}
            @forelse($articles as $article)
                <div class="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300">
                    @if($article->image)
                        <img src="{{ $article->image }}" alt="{{ $article->title }}" class="w-full h-48 object-cover mb-4 rounded">
                    @endif
                    <h3 class="text-xl font-semibold mb-2">{{ $article->title }}</h3>
                    <p class="text-gray-600 mb-4">{{ Str::limit($article->content, 120) }}</p> {{-- [cite: 1273] --}}
                    <a href="{{ route('articles.show', $article) }}" class="text-blue-600 hover:underline font-bold">Read More &rarr;</a> {{-- [cite: 1274] --}}
                </div>
            @empty
                <p class="text-gray-500">No articles found.</p>
            @endforelse
        </div>
        {{-- Pagination links will go here later --}}
    </div>
    
    @endsection
    
  5. Create Initial Show View (resources/views/articles/show.blade.php): Displays a single article.

     @extends('layouts.app')
    
     @section('content')
     <div class="bg-white p-8 rounded-lg shadow-md max-w-4xl mx-auto">
         @if($article->image)
             <img src="{{ $article->image }}" alt="{{ $article->title }}" class="w-full h-64 object-cover mb-6 rounded">
         @endif
         <h1 class="text-4xl font-bold mb-4">{{ $article->title }}</h1>
         <div class="prose lg:prose-xl max-w-none"> {{-- Use Tailwind Typography --}}
             {!! nl2br(e($article->content)) !!} {{-- Display content with line breaks --}}
         </div>
         <div class='mt-8'>
             <a href="{{ route('articles.index') }}" class="text-blue-600 hover:underline font-bold">&larr; Back to Articles</a>
         </div>
     </div>
     @endsection
    
  6. Define Routes (routes/web.php): Tell Laravel how to handle URLs for articles.

    <?php
    
    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\ArticleController; // Import controller
    
    Route::get('/', function () {
        // Maybe redirect to articles index?
        return redirect()->route('articles.index');
        // return view('welcome');
    });
    
    // Use resourceful routing for articles
    Route::resource('articles', ArticleController::class)->only([
        'index', 'show' // Only enable index and show routes for now
    ]);
    

    Using Route::resource with only() is a concise way to define standard CRUD routes[cite: 1289]. This sets up:

    • GET /articles -> ArticleController@index (named articles.index)
    • GET /articles/{article} -> ArticleController@show (named articles.show)
  7. Preview: Visit http://127.0.0.1:8000/articles. You should see your index page with the dummy articles[cite: 1291, 1292]. Clicking "Read More" should take you to the single article view. Okay, basic structure done!

Integrating Livewire

Now for the magic! We'll replace the static parts with dynamic Livewire components. Remember, we already added @livewireStyles and @livewireScripts to our main layout (layouts/app.blade.php)[cite: 1295].

  1. Create the Articles Index Livewire Component:

    php artisan make:livewire ArticlesIndex
    

    [cite: 1295] This creates:

    • app/Livewire/ArticlesIndex.php [cite: 1296]
    • resources/views/livewire/articles-index.blade.php [cite: 1296]
  2. Implement Pagination in the Component Class (app/Livewire/ArticlesIndex.php): Modify the class to fetch articles with pagination.

    <?php
    
    namespace App\Livewire;
    
    use Livewire\Component;
    use App\Models\Article;
    use Livewire\WithPagination; // Import pagination trait
    
    class ArticlesIndex extends Component
    {
        use WithPagination; // Use the trait
    
        // Optional: Use Bootstrap theme for pagination links if needed
        // protected $paginationTheme = 'bootstrap';
    
        public function render()
        {
            return view('livewire.articles-index', [
                // Fetch articles, order by latest, paginate 6 per page
                'articles' => Article::latest()->paginate(6) [cite: 1301]
            ]);
        }
    }
    

    We added use Livewire\WithPagination; and use WithPagination; [cite: 1300] and changed the query to use paginate(6)[cite: 1301].

  3. Update the Component View (resources/views/livewire/articles-index.blade.php): Move the article listing loop and add pagination links here. Crucially, add wire:navigate to the "Read More" links for seamless navigation[cite: 1303].

    <div> {{-- Livewire components must have a single root element --}}
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            @forelse($articles as $article)
                <div class="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300">
                    @if($article->image)
                        <img src="{{ $article->image }}" alt="{{ $article->title }}" class="w-full h-48 object-cover mb-4 rounded">
                    @endif
                    <h3 class="text-xl font-semibold mb-2">{{ $article->title }}</h3>
                    <p class="text-gray-600 mb-4">{{ Str::limit($article->content, 120) }}</p>
                    {{-- Use wire:navigate for SPA-like navigation --}}
                    <a wire:navigate href="{{ route('articles.show', $article) }}" class="text-blue-600 hover:underline font-bold">Read More &rarr;</a> {{-- [cite: 1303] --}}
                </div>
            @empty
                <p class="text-gray-500">No articles found.</p>
            @endforelse
        </div>
    
        <div class="mt-8">
            {{-- Render pagination links --}}
            {{ $articles->links() }} {{-- [cite: 1304] --}}
        </div>
    </div>
    
  4. Update the Main Index View (resources/views/articles/index.blade.php): Replace the static article loop with the Livewire component tag.

    @extends('layouts.app')
    
    @section('content')
        <h2 class='my-4 text-3xl font-bold'>Articles</h2>
    
        {{-- Render the Livewire component --}}
        @livewire('articles-index')
        {{-- Or: <livewire:articles-index /> --}}
    
    @endsection
    

    [cite: 1305]

  5. Try It Out (Pagination): Refresh /articles. You should see the articles and pagination links[cite: 1306, 1307]. Clicking the pagination links (e.g., "2", "Next") should update the article list without a full page reload[cite: 1308]. Check your browser's network tools; you'll see background AJAX requests fetching the next set of articles[cite: 1309]. Cool! [cite: 1310]

Showing the Single Blog Post with Livewire

Let's apply the same approach to the single article view for seamless navigation.

  1. Create the Article Show Livewire Component:

    php artisan make:livewire ArticleShow
    

    [cite: 1312] This creates:

    • app/Livewire/ArticleShow.php [cite: 1312]
    • resources/views/livewire/article-show.blade.php [cite: 1312]
  2. Update the Component Class (app/Livewire/ArticleShow.php): We need to accept the Article model, likely passed via the route. Livewire can often automatically resolve this (Route Model Binding), but using mount() makes it explicit.

    <?php
    
    namespace App\Livewire;
    
    use Livewire\Component;
    use App\Models\Article; // Import model
    
    class ArticleShow extends Component
    {
        public Article $article; // Declare public property to hold the article
    
        // Use mount() to receive the article from the route
        public function mount(Article $article)
        {
            $this->article = $article; [cite: 1314]
        }
    
        public function render()
        {
            // Render the view, Livewire makes $article available automatically
            return view('livewire.article-show'); [cite: 1315]
        }
    }
    
    

    (Note: The source uses public $article; without type hinting, but modern PHP/Laravel encourages type hinting like public Article $article;)[cite: 1313]. The mount method explicitly assigns the model passed from the route to the component's public property[cite: 1313, 1314].

  3. Update the Component View (resources/views/livewire/article-show.blade.php): Display the article content and add a wire:navigate link back to the index.

    <div class="bg-white p-8 rounded-lg shadow-md max-w-4xl mx-auto">
        @if($article->image)
            <img src="{{ $article->image }}" alt="{{ $article->title }}" class="w-full h-64 object-cover mb-6 rounded">
        @endif
        <h1 class="text-4xl font-bold mb-4">{{ $article->title }}</h1>
        <div class="prose lg:prose-xl max-w-none">
            {!! nl2br(e($article->content)) !!}
        </div>
        <div class='mt-8'>
            {{-- Use wire:navigate for the back link --}}
            <a wire:navigate href="{{ route('articles.index') }}" class="text-blue-600 hover:underline font-bold">&larr; Back to Articles</a>
        </div>
    </div>
    

    [cite: 1316]

  4. Update Routes for Livewire Components (routes/web.php): Instead of routing to the ArticleController, we now route directly to our Livewire components. Livewire components routed this way are called "Full Page Components".

    <?php
    
    use Illuminate\Support\Facades\Route;
    // Import Livewire components
    use App\Livewire\ArticlesIndex;
    use App\Livewire\ArticleShow;
    
    Route::get('/', function () {
        return redirect()->route('articles.index');
    });
    
    // Route directly to Livewire components
    Route::get('/articles', ArticlesIndex::class)->name('articles.index'); // [cite: 1317, 1318]
    Route::get('/articles/{article}', ArticleShow::class)->name('articles.show'); // [cite: 1317, 1318]
    
    

    (Note: We removed the ArticleController import and usage here, replacing it with direct routing to the Livewire components. Added names for clarity).

  5. Create Livewire Layout (resources/views/components/layouts/app.blade.php): Full Page Livewire components need a specific layout file located in resources/views/components/layouts/. By default, they look for app.blade.php. This layout uses {{ $slot }} to render the component's content. Let's copy our existing layouts/app.blade.php and modify it.

    <!DOCTYPE html>
    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        {{-- Use title from component or default --}}
        <title>{{ $title ?? 'Laravel Livewire Blog' }}</title>
        <script src="[https://cdn.tailwindcss.com?plugins=typography](https://cdn.tailwindcss.com?plugins=typography)"></script>
        @livewireStyles
    </head>
    <body class="bg-gray-100 text-gray-800 font-sans">
    
        <nav class="bg-blue-600 text-white p-4">
            <div class="container mx-auto flex justify-between items-center">
                 <a wire:navigate href="/articles" class="text-2xl font-bold hover:text-blue-200">Laravel Livewire Blog</a> </div>
        </nav>
    
        <main class="container mx-auto mt-8 p-4">
            {{ $slot }} {{-- Livewire component content goes here --}} </main>
    
        <footer class="bg-gray-700 text-white p-4 mt-8">
            <div class="container mx-auto text-center">
                © {{ date('Y') }} Laravel Livewire Blog. </div>
        </footer>
    
        @livewireScripts
    </body>
    </html>
    

    (The main change is replacing @yield('content') with {{ $slot }})[cite: 1322]. Also ensured the nav link uses wire:navigate.

  6. Try It Out (Full Navigation): Go back to /articles. Click "Read More" on an article. The page should transition instantly to the single article view without a full browser reload. Click "Back to Articles". Again, instant transition back to the index. Pretty neat, eh? [cite: 1323]

That’s All For Now (On The Basics)

This tutorial covers the fundamentals of using Livewire to build a reactive blog index and single post view in Laravel[cite: 1324]. You've seen how wire:navigate enables seamless transitions and how pagination works dynamically. Feel free to explore and expand on this foundation[cite: 1325]!

How Does It Work? Behind the Scenes

Full Page Components & wire:navigate

We used Livewire's "Full Page Components" feature, routing directly to our ArticlesIndex and ArticleShow components[cite: 1326, 1327].

  • Initial Load: When you first visit /articles or /articles/{id}, Laravel renders the Livewire component server-side within the component layout (components/layouts/app.blade.php). This ensures the initial page is fully formed HTML, which is great for SEO[cite: 1327, 1328].
  • Subsequent Navigation (wire:navigate): When you click a link marked with wire:navigate, Livewire intercepts the click. Instead of a full page reload, it makes an AJAX request in the background to get the HTML content of the next Livewire component[cite: 1328, 1329]. It then intelligently swaps out the relevant parts of the current page's DOM with the new content, creating that smooth, SPA-like transition[cite: 1330].
  • Component Interactions (Pagination): When you click a pagination link within the ArticlesIndex component, Livewire sends an AJAX request back to the same component (ArticlesIndex). The server re-renders just that component with the new data (the next page of articles). Livewire receives the updated HTML fragment and morphs the existing component's DOM on the page to match, again avoiding a full reload[cite: 1330].

Future Improvements

  • Finer-Grained Updates: Currently, wire:navigate replaces the whole component slot. For more complex UIs, you might explore techniques (potentially with tools like Alpine.js alongside Livewire, or using nested components and events) to update only specific parts of the page instead of the entire component body[cite: 1331, 1332, 1333].
  • Editing & Authentication: Add user login and allow authenticated users to create/edit posts using Livewire forms and actions[cite: 1333].
  • Full Tailwind Integration: Set up Tailwind CSS locally using Vite for better customization and purging unused styles, rather than relying on the CDN[cite: 1277, 1333].

Livewire Insights

Insight #1: Cached Computed Properties

For data that doesn't change often (like article content), Livewire's computed properties with caching can boost performance[cite: 1334, 1335].

// In a Livewire component class
use Illuminate\Support\Facades\Cache; // Import Cache facade
use Livewire\Attributes\Computed; // Import Computed attribute

// ...

#[Computed(persist: true, seconds: 3600)] // Cache for 1 hour
public function articles()
{
    // This result will be cached
    return Article::all();
}

public function render()
{
    // Access the cached property in the view
    return view('livewire.show-articles', [
        'articles' => $this->articles // Property access triggers computed method
    ]);
}
Robin Metcalfe

About the Author: Robin Metcalfe

Robin is a freelance web strategist and developer based in Edinburgh, with over 15 years of experience helping businesses build effective and engaging online platforms using technologies like Laravel and WordPress.

Get in Touch