Laravel for Beginners #5 - Create the Frontend
Tags: laravel, php, webdev, tutorial
Published: 2023-04-03 15:00:00

> [Download source code here.](https://www.ericsdevblog.com/posts/laravel-for-beginners-1?ref=Dev_Community/) ⬅️

In the final article of this tutorial, we are going to create the frontend part of the app. First of all, let's make a plan.

For this blog application, we'll have a homepage displaying a list of all recent posts, a category page displaying a list of posts under a specific category, a tag page displaying a list of posts with a specific tag, a post page displaying the content of a specific post, and lastly, a search page displaying a list of posts based on a search query.

All of these pages will have a sidebar with a search box, a list of categories and tags. The post page will also have a related posts section at the bottom.

We've dealt with the database and the models in the previous article, so here we'll start with the routes.

## Create the routes

`routes/web.php`

```php
. . .

// Homepage
Route::get('/', [PostController::class, 'home'])->name('home');

// A list of posts under this category
Route::get('/category/{category}', [CategoryController::class, 'category'])->name('category');

// A list of posts with this tag
Route::get('/tag/{tag}', [TagController::class, 'tag'])->name('tag');

// Display a single post
Route::get('/post/{post}', [PostController::class, 'post'])->name('post');

// A list of posts based on search query
Route::post('/search', [PostController::class, 'search'])->name('search');

. . .
```

## The homepage

Each of these routes has a corresponding controller method. We'll start with the `home()` controller:

`app/Http/Controllers/PostController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;

class PostController extends Controller
{
    /**
     * Display the home page
     */
    public function home(): View
    {
        $posts = Post::where('is_published', true)->paginate(env('PAGINATE_NUM'));
        $categories = Category::all();
        $tags = Tag::all();

        return view('home', [
            'posts' => $posts,
            'categories' => $categories,
            'tags' => $tags
        ]);
    }

    . . .
}
```

Line 22, there are two things you need to note here.

First, `where('is_published', true)` makes sure that only published articles are retrieved.

And second, `paginate()` method is one of Laravel's built-in method, allowing you to easily create pagination in your app. The `paginate()` takes an integer as input. For example, `paginate(10)` means 10 items will be displayed on each page. Since this input variable will be used on many pages, you can create an environmental variable in the `.env` file, and then you can retrieve it anywhere using `env()` method.

`.env`

```text
. . .
PAGINATE_NUM=12
```

Next, let's create the corresponding homepage view. [Here](https://github.com/ericsdevblog/blog-template) is the template structure I've created, and this is the view structure:

```text
resources/views
├── category.blade.php
├── home.blade.php
├── layout.blade.php
├── post.blade.php
├── search.blade.php
├── tag.blade.php
├── vendor
│   ├── list.blade.php
│   └── sidebar.blade.php
└── welcome.blade.php
```

We'll start with the `layout.blade.php`:

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

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    @yield('title')
</head>

<body class="container mx-auto font-serif">
    <nav class="flex flex-row justify-between h-16 items-center border-b-2">
        <div class="px-5 text-2xl">
            <a href="/"> My Blog </a>
        </div>
        <div class="hidden lg:flex content-between space-x-10 px-10 text-lg">
            <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1">GitHub</a>
            <a href="{{ route('dashboard') }}" class="hover:underline hover:underline-offset-1">Dashboard</a>
            <a href="#" class="hover:underline hover:underline-offset-1">Link</a>
        </div>
    </nav>

    @yield('content')

    <footer class="bg-gray-700 text-white">
        <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10">
            <p class="font-serif text-center mb-3 sm:mb-0">
                Copyright ©
                <a href="https://www.ericsdevblog.com/" class="hover:underline">Eric Hu</a>
            </p>

            <div class="flex justify-center space-x-4">
                . . .
            </div>
        </div>
    </footer>
</body>

</html>
```

Line 8, by default, Laravel uses Vite for asset bundling. In the previous article, we have installed Laravel Breeze, which uses TailwindCSS. This line of code will automatically import the corresponding `app.css` and `app.js` for you.

You can use a different framework of course, but you'll have to consult the respective documentations for details on how to use them with Laravel or Vite.

Next, for the homepage:

`resources/views/home.blade.php`

```html
@extends('layout')

@section('title')
<title>Home</title>
@endsection

@section('content')
<div class="grid grid-cols-4 gap-4 py-10">
    <div class="col-span-3 grid grid-cols-1">
        @include('vendor.list')
    </div>
    @include('vendor.sidebar')
</div>
@endsection
```

And the post list:

`resources/views/vendor/list.blade.php`

```html
<!-- List of posts -->
<div class="grid grid-cols-3 gap-4">
    @foreach ($posts as $post)
    <!-- post -->
    <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
        <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a>
        <div class="m-4 grid gap-2">
            <div class="text-sm text-gray-500">
                {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}
            </div>
            <h2 class="text-lg font-bold">{{ $post->title }}</h2>
            <p class="text-base">
                {{ Str::limit(strip_tags($post->content), 150, '...') }}
            </p>
            <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a>
        </div>
    </div>
    @endforeach
</div>

{{ $posts->links() }}
```

Line 6, `Storage::url($post->cover)` generates the URL that points to the cover image.

Line 9, the way Laravel stores timestamps is not user friendly, so here we are using Carbon to reformat the timestamps.

Line 13, here we are using `strip_tags()` to remove the HTML tags, and then `limit()` sets the maximum length of the string, the excessive part will be replaced with `...`.

Line 21, remember be used `paginate()` method to create paginator in the controller? this is how we can display the paginator in the view.

As well as the sidebar:

`resources/views/vendor/sidebar.blade.php`

```html
<div class="col-span-1">
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Search</div>
    <div class="p-4">
      <form action="{{ route('search') }}" method="POST" class="grid grid-cols-4 gap-2">
        {{ csrf_field() }}
        <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." />
        <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1">
          Search
        </button>
      </form>
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Categories</div>
    <div class="p-4">
      <ul class="list-none list-inside">
        @foreach ($categories as $category)
        <li>
          <a href="{{ route('category', ['category' => $category->id]) }}" class="text-blue-500 hover:underline">{{ $category->name }}</a>
        </li>
        @endforeach
      </ul>
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">Tags</div>
    <div class="p-4">
      @foreach ($tags as $tag)
      <span class="mr-2"><a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline">{{ $tag->name }}</a></span>
      @endforeach
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">More Card</div>
    <div class="p-4">
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat,
        voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic
        reprehenderit pariatur autem totam, voluptates non officia accusantium
        rerum unde provident!
      </p>
    </div>
  </div>
  <div class="border rounded-md mb-4">
    <div class="bg-slate-200 p-4">...</div>
    <div class="p-4">
      <p>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat,
        voluptatem ab tempore recusandae sequi libero sapiente autem! Sit hic
        reprehenderit pariatur autem totam, voluptates non officia accusantium
        rerum unde provident!
      </p>
    </div>
  </div>
</div>
```

## The category page

`app/Http/Controllers/CategoryController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class CategoryController extends Controller
{
    /**
     * Display a list of posts belong to the category
     */
    public function category(string $id): View
    {
        $category = Category::find($id);
        $posts = $category->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM'));
        $categories = Category::all();
        $tags = Tag::all();

        return view('category', [
            'category' => $category,
            'posts' => $posts,
            'categories' => $categories,
            'tags' => $tags
        ]);
    }

    . . .
}
```

`resources/views/category.blade.php`

```html
@extends('layout')

@section('title')
<title>Category - {{ $category->name }}</title>
@endsection

@section('content')
<div class="grid grid-cols-4 gap-4 py-10">
    <div class="col-span-3 grid grid-cols-1">
        @include('vendor.list')
    </div>
    @include('vendor.sidebar')
</div>
@endsection
```

## The tag page

`app/Http/Controllers/TagController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TagController extends Controller
{
    /**
     * Display a list of posts belong to the category
     */
    public function tag(string $id): View
    {
        $tag = Tag::find($id);
        $posts = $tag->posts()->where('is_published', true)->paginate(env('PAGINATE_NUM'));
        $categories = Category::all();
        $tags = Tag::all();

        return view('tag', [
            'tag' => $tag,
            'posts' => $posts,
            'categories' => $categories,
            'tags' => $tags
        ]);
    }
    . . .
}
```

`resources/views/tag.blade.php`

```html
@extends('layout')

@section('title')
<title>Tag - {{ $tag->name }}</title>
@endsection

@section('content')
<div class="grid grid-cols-4 gap-4 py-10">
    <div class="col-span-3 grid grid-cols-1">
        @include('vendor.list')
    </div>
    @include('vendor.sidebar')
</div>
@endsection
```

## The post page

`app/Http/Controllers/PostController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;

class PostController extends Controller
{
    . . .

    /**
     * Display requested post
     */
    public function post(string $id): View
    {
        $post = Post::find($id);
        $categories = Category::all();
        $tags = Tag::all();
        $related_posts = Post::where('is_published', true)->whereHas('tags', function (Builder $query) use ($post) {
            return $query->whereIn('name', $post->tags->pluck('name'));
        })->where('id', '!=', $post->id)->take(3)->get();

        return view('post', [
            'post' => $post,
            'categories' => $categories,
            'tags' => $tags,
            'related_posts' => $related_posts
        ]);
    }

    . . .
}
```

Line 27-29, this is how we are able to retrieve related posts. The idea is to get the posts with the same tags. This chain of methods looks kind of scary, but don't worry, let's analyze them one by one.

The first method is easy, `where('is_published', true)` returns all the posts that are published.

The `whereHas()` method is where things get complicated. To understand `whereHas()` we need to first talk about `has()`. `has()` is a Laravel Eloquent method that allows us to check the existence of a relationship. For example:

```php
$posts = Post::has('comments', '>', 3)->get();
```

This code will retrieve all the posts that have more than 3 comments. Notice that you cannot use `where()` to do this because `comments` is not a column in the `posts` table, it is another table that has a relation with `posts`.

`whereHas()` works just like `has()`, only it offers a little more power. Its second parameter is a function that allows us to inspect the content that "another table", which in our case, is the `tags` table. We can access the `tags` table through the variable `$q`.

Line 28, the `whereIn()` method takes two parameters, the first one is the specified column, the second is an array of acceptable values. The method will return the records with only the acceptable values and exclude the rest.

The rest should be very easy to understand. `where('id', '!=', $post->id)` exclude the current post, and `take(3)` takes the first three records.

`resources/views/post.blade.php`

```html
@extends('layout')

@section('title')
<title>Page Title</title>
@endsection

@section('content')
<div class="grid grid-cols-4 gap-4 py-10">
    <div class="col-span-3">

        <img class="rounded-md object-cover h-96 w-full" src="{{ Storage::url($post->cover) }}" alt="..." />
        <h1 class="mt-5 mb-2 text-center text-2xl font-bold">{{ $post->title }}</h1>
        <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ $post->user->name }} | {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}</p>

        <div>{!! $post->content !!}</div>

        <div class="my-5">
            @foreach ($post->tags as $tag)
            <a href="{{ route('tag', ['tag' => $tag->id]) }}" class="text-blue-500 hover:underline" mr-3">#{{ $tag->name }}</a>
            @endforeach
        </div>

        <hr>

        <!-- Related posts -->

        <div class="grid grid-cols-3 gap-4 my-5">
            @foreach ($related_posts as $post)
            <!-- post -->
            <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md">
                <a href="{{ route('post', ['post' => $post->id]) }}"><img class="rounded-t-md object-cover h-60 w-full" src="{{ Storage::url($post->cover) }}" alt="..." /></a>
                <div class="m-4 grid gap-2">
                    <div class="text-sm text-gray-500">
                        {{ \Carbon\Carbon::parse($post->created_at)->format('M d, Y') }}
                    </div>
                    <h2 class="text-lg font-bold">{{ $post->title }}</h2>
                    <p class="text-base">
                        {{ Str::limit(strip_tags($post->content), 150, '...') }}
                    </p>
                    <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{{ route('post', ['post' => $post->id]) }}">Read more →</a>
                </div>
            </div>
            @endforeach
        </div>

    </div>
    @include('vendor.sidebar')
</div>
@endsection
```

Line 15, `{!! $post->content !!}` is Laravel's safe mode. It tells Laravel to render HTML tags instead of displaying them as plain text.

## The search page

`app/Http/Controllers/PostController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;

class PostController extends Controller
{
    . . .

    /**
     * Display search result
     */
    public function search(Request $request): View
    {
        $key = $request->input('q');
        $posts = Post::where('title', 'like', "%{$key}%")->orderBy('id', 'desc')->paginate(env('PAGINATE_NUM'));
        $categories = Category::all();
        $tags = Tag::all();

        return view('search', [
            'key' => $key,
            'posts' => $posts,
            'categories' => $categories,
            'tags' => $tags,
        ]);
    }

    . . .
}
```

`resources/views/search.blade.php`

```html
@extends('layout')

@section('title')
<title>Search - {{ $key }}</title>
@endsection

@section('content')
<div class="grid grid-cols-4 gap-4 py-10">
    <div class="col-span-3 grid grid-cols-1">
        @include('vendor.list')
    </div>
    @include('vendor.sidebar')
</div>
@endsection
```

> If you liked this article, please also check out my other tutorials:
> - [Django for Beginners](https://www.ericsdevblog.com/posts/django-for-beginners-1?ref=Dev_Community/)
> - [Vue.js for Beginners](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1?ref=Dev_Community/)
> - [Create a Modern Application with Django and Vue](https://www.ericsdevblog.com/posts/create-a-modern-application-with-django-and-vue-1?ref=Dev_Community/)
> - [Beginner’s Roadmap to Web Development](https://www.ericsdevblog.com/series/beginners-roadmap-to-web-development?ref=Dev_Community/)