Laravel 8

Créer un blog – la page d’accueil

Nous avons dans le précédent article mis en place les migrations essentielles ainsi que la population. Maintenant que nous avons du contenu nous allons pouvoir passer au contenant. Dans un premier temps nous allons installer un package pour la gestion des medias. Ensuite on utilisera le thème Calvin en l’adaptant à Laravel pour créer notre page d’accueil. Pour rappel le code de départ du présent article est le code final du précédent que vous pouvez télécharger en y retournant.

Vous pouvez télécharger le code final de cet article ici.

Les medias

Pour la gestion des medias on va utiliser l’excellent package Laravel File Manager. On l’installe :

composer require unisharp/laravel-filemanager

Ensuite on publie la configuration est les assets :

php artisan vendor:publish --tag=lfm_config
php artisan vendor:publish --tag=lfm_public

On crée un lien symbolique :

php artisan storage:link

Ainsi le dossier public\storage est relié au dossier storage\app\public.

Pour terminer on ajoute les routes dans routes/web.php :

use UniSharp\LaravelFilemanager\Lfm;

Route::group(['prefix' => 'laravel-filemanager', 'middleware' => 'auth'], function () {
    Lfm::routes();
});

Vous pouvez maintenant accéder à la démo en vous connectant (puisqu’on a prévu le middleware auth). Puisqu’on a installé Breeze vous pouvez accéder à la page de connexion avec l’url monblog.ext/login. (ext représente l’extension que vous utilisez pour votre développement). Comme on a créé des utilisateurs dans le précédent article vous pouvez aller trouver un email dans la base de données, tous les mots de passe sont password.

La démo est accessible à l’adresse monblog.ext/laravel-filemanager/demo :

Si ça ne fonctionne pas chez vous reprenez la procédure d’installation.

Par défaut vous avez ces dossiers de créés :

Si vous vous connectez avec l’utilisateur qui a l’id 2 et que vous chargez une image vous aller voir la création de ces dossiers :

Le principe est simple : les images sont toutes dans le dossier photos. Chaque utilisateur a son dossier dont le nom est son identifiant. On a deux versions de l’image : une réduite dans thumbs et une normale. On a les réglages pour l’image réduite dans la configuration (config/lfm.php) :

/*
|--------------------------------------------------------------------------
| Thumbnail
|--------------------------------------------------------------------------
  */

// If true, image thumbnails would be created during upload
'should_create_thumbnails' => true,

'thumb_folder_name'        => 'thumbs',

// Create thumbnails automatically only for listed types.
'raster_mimetypes'         => [
    'image/jpeg',
    'image/pjpeg',
    'image/png',
],

'thumb_img_width'          => 200, // px

'thumb_img_height'         => 200, // px

On va conserver ces réglages. Pour notre page d’accueil j’ai prévu des images que vous pouvez aller récupérer dans le fichier ZIP à télécharger. Il vous suffit de copier le dossier photos et son contenu :

Vous aurez ainsi les images pour les 9 articles qu’on a déjà créés.

Calvin

Vous avez normalement téléchargé le thème Calvin et vous devez disposer de tout ça :

On ne va évidemment pas tout prendre. On aura besoin des 2 fichiers CSS :

Vous allez donc les copier dans public/css :

Le fichier app.css correspond aux règles de style de Breeze, on le supprimera quand on aura basculé les vues de l’authentification avec nos nouvelles versions.

On va aussi avoir besoin de fichiers Javascript :

On les copie dans notre dossier :

Le fichier app.jss correspond aussi à Breeze, on le supprimera également quand on aura basculé.

En ce qui concerne JQuery on utilisera un CDN, ça sera plus efficace. Il est aussi possible qu’à la fin on condense tout ça dans un seul fichier.

Vous avez aussi dans Calvin les fichiers suivants :

Les icônes représentent un « C » qui est l’initiale de Calvin. J’ai créé les fichiers équivalents avec un « B », vous pouvez les récupérer dans le dossier ZIP. Copiez-les dans notre dossier public.

Pour terminer on va récupérer le fichier index.html, le renommer layout.blade.php, créer un dossier views/front et enregistrer le fichier dans ce dossier :

Il va falloir évidemment lui apporter pas mal de modifications…

Un contrôleur et un repository pour les articles

On a déjà un contrôleur PostController qu’on a créé précédemment en même temps que le modèle et les migrations :

Pour bien séparer avec le backend on va créer un dossier Front et le mettre dedans (attention au changement d’espace de nom) :

Comme on aura pas mal de manipulations au niveau de la base de données on va aussi prévoir un repository :

Pour définir le nombre d’articles à afficher sur chaque page on prévoit une information dans le fichier config.app :

/*
|--------------------------------------------------------------------------
| Pagination Configuration
|--------------------------------------------------------------------------
*/

'nbrPages' => [
    'posts' => 6,
],

On code le repository

Au niveau du repository on va avoir besoin :

  • des articles actifs classés par date paginés
  • des informations des derniers articles pour le diaporama (en langage Calvin ça s’appelle des heros)

Les articles paginés

On va commencer par les articles classés et paginés :

<?php

namespace App\Repositories;
use App\Models\Post;

class PostRepository
{
    protected function queryActive()
    {
        return Post::select(
                      'id',
                      'slug',
                      'image',
                      'title',
                      'excerpt',
                      'user_id')
                    ->with('user:id,name')
                    ->whereActive(true);
    }

    protected function queryActiveOrderByDate()
    {
        return $this->queryActive()->latest();
    }

    public function getActiveOrderByDate($nbrPages)
    {
        return $this->queryActiveOrderByDate()->paginate($nbrPages);
    }
}

J’ai séparé en 3 fonctions parce qu’on aura besoin de ce découpage pour les autres fonctionnalités. La fonction d’entrée est getActiveOrderByDate. On lui transmet le nombre de pages et elle renvoie les articles concernés. On utilise un SELECT pour éviter de charger le contenu des articles qui peut être volumineux et qui est inutile pour la page d’accueil. On charge aussi (eager loading) pour chaque article le nom de son auteur pour l’afficher).

Les heros

Les heros sont les 5 derniers articles créés ou modifiés, on ajoute donc cette fonction :

public function getHeros()
{
    return $this->queryActive()->with('categories')->latest('updated_at')->take(5)->get();
}

On charge aussi les catégories parce qu’on doit les afficher.

On code le contrôleur et la route

Le contrôleur va utiliser le repository :

<?php

namespace App\Http\Controllers\Front;
use App\Repositories\PostRepository;
use App\Http\Controllers\Controller;

use Illuminate\Http\Request;

class PostController extends Controller
{
    protected $postRepository;
    protected $nbrPages;

    public function __construct(PostRepository $postRepository)
    {
        $this->postRepository = $postRepository;
        $this->nbrPages = config('app.nbrPages.posts');
    }

    public function index()
    {
        $posts = $this->postRepository->getActiveOrderByDate($this->nbrPages);
        $heros = $this->postRepository->getHeros();

        return view('front.index', compact('posts', 'heros'));
    }
}

Pour la route on supprime celle de Breeze :

Route::get('/', function () {
    return view('welcome');
});

Et on crée la nôtre pour pointer sur le contrôleur :

use App\Http\Controllers\Front\PostController as FrontPostController;

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

Le layout

C’est dans les vues qu’on va avoir le plus de travail. Récupérez le fichier index.html de Calvin et copiez-le ici en changeant son nom et en créant le dossier front :

On va traiter ce layout par zones.

Le head

On a ce code :

<!DOCTYPE html>
<html class="no-js" lang="en">
<head>

    <!--- basic page needs
    ================================================== -->
    <meta charset="utf-8">
    <title>Calvin</title>
    <meta name="description" content="">
    <meta name="author" content="">

    <!-- mobile specific metas
    ================================================== -->
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSS
    ================================================== -->
    <link rel="stylesheet" href="css/vendor.css">
    <link rel="stylesheet" href="css/styles.css">

    <!-- script
    ================================================== -->
    <script src="js/modernizr.js"></script>
    <script defer src="js/fontawesome/all.min.js"></script>

    <!-- favicons
    ================================================== -->
    <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
    <link rel="manifest" href="site.webmanifest">

</head>

On va renseigner la langue selon la locale :

<html class="no-js" lang="{{ str_replace('_', '-', app()->getLocale()) }}">

Pour le titre on va aller le chercher dans la configuration :

<title>{{ config('app.name') }}</title>

Pour le CSS on utilise l’helper pour générer l’url complète et on prévoit un emplacement pour les les vues qui vont utiliser ce layout puissent ajouter du style :

<link rel="stylesheet" href="{{ asset('css/vendor.css') }}">
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
@yield('style')

Pour le Javascript on utilise aussi l’helper :

<script src="{{ asset('js/modernizr.js') }}"></script>
<script defer src="{{ asset('js/fontawesome/all.min.js') }}"></script>

Pareil pour les icônes :

<link rel="apple-touch-icon" sizes="180x180" href="{{ asset('apple-touch-icon.png') }}">
<link rel="icon" type="image/png" sizes="32x32" href="{{ asset('favicon-32x32.png') }}">
<link rel="icon" type="image/png" sizes="16x16" href="{{ asset('favicon-16x16.png') }}">
<link rel="manifest" href="{{ asset('site.webmanifest') }}">

Le header

Le header contient la barre de navigation :

<header class="s-header">

    <div class="s-header__logo">
        <a class="logo" href="index.html">
            <img src="images/logo.svg" alt="Homepage">
        </a>
    </div>

    <div class="row s-header__navigation">

        <nav class="s-header__nav-wrap">

            <h3 class="s-header__nav-heading h6">Navigate to</h3>

            <ul class="s-header__nav">
                ...
            </ul> <!-- end s-header__nav -->

            <a href="#0" title="Close Menu" class="s-header__overlay-close close-mobile-menu">Close</a>

        </nav> <!-- end s-header__nav-wrap -->

    </div> <!-- end s-header__navigation -->

    <a class="s-header__toggle-menu" href="#0" title="Menu"><span>Menu</span></a>

    <div class="s-header__search">

        <div class="s-header__search-inner">
            <div class="row wide">

                <form role="search" method="get" class="s-header__search-form" action="#">
                    ...
                </form>

                <a href="#0" title="Close Search" class="s-header__overlay-close">Close</a>

            </div> <!-- end row -->
        </div> <!-- s-header__search-inner -->

    </div> <!-- end s-header__search wrap -->	

    <a class="s-header__search-trigger" href="#">
        ...
    </a>

</header>

Qui correspond à cette zone :

Il va déjà falloir changer le logo, j’en ai créé un que vous pouvez trouver dans le fichier ZIP, copiez-le ici :

Dans le code on renseigne l’url de la page d’accueil et on utilise l’helper pour l’url du logo :

<div class="s-header__logo">
    <a class="logo" href="{{ route('home') }}">
        <img src="{{ asset('images/logo.svg') }}" alt="Homepage">
    </a>
</div>

On s’occupera du menu plus loin, on va donc conserver le reste du code inchangé.

La zone hero

La zone suivante est celle du diaporama (les heros).

On y trouve donc le diaporama mais également des icônes sociales. On va ici se contenter de prévoir un emplacement qu’on remplira avec la vue index :

<!-- hero
================================================== -->
@yield('hero')

Le content

C’est là qu’on a le résumé des articles :

On va aussi juste prévoir un emplacement :

<!-- content
================================================== -->
<section class="s-content s-content--no-top-padding">

    @yield('main')

</section>

Le footer

Pour le moment on ne va rien faire dans le footer et se contenter de le conserver tel quel.

Le Javascript

Pour le Javascript en bas de page on va utiliser un CDN pour JQuery et l’helper pour les urls des fichiers. On va aussi ajouter un emplacement pour pouvoir ajouter du code dans les vues enfants :

<!-- JavaScript
================================================== -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="{{ asset('js/plugins.js') }}"></script>
<script src="{{ asset('js/main.js') }}"></script>
@yield('scripts')

Un composeur de vue

Comme on aura besoin d’informations systématiques dans le layout, mais aussi dans la vue index on va créer un composeur de vue :

Avec ce code :

<?php

namespace App\Http\ViewComposers;

use Illuminate\View\View;
use App\Models\Category;

class HomeComposer
{
    /**
     * Bind data to the view.
     *
     * @param  View  $view
     * @return void
     */
    public function compose(View $view)
    {
        $view->with([
            'categories' => Category::has('posts')->get(),
        ]);
    }
}

On envoie dans la vue les catégories qui possèdent des articles. Il n’existe à ma connaissance pas de commande Artisan pour générer ces classes.

Il faut ensuite utiliser ce composeur de vue dans AppServiceProvider :

use App\Http\ViewComposers\HomeComposer;
use Illuminate\Support\Facades\View;

...

public function boot()
{
    View::composer(['front.layout', 'front.index'], HomeComposer::class);
}

Maintenant on est sûr d’avoir les catégories dans ces deux vues.

Le vue index

Maintenant qu’on a un layout présentable on va créer la vue index :

 

On va avoir cette structure :

@extends('front.layout')


@section('hero')

@endsection


@section('main')

@endsection

Les heros

On va s’occuper du diaporama. On a vu qu’on envoie les données à partir du contrôleur et qu’on dispose ainsi d’une variable $heros.

Un composant

Comme on va avoir du code répétitif on crée un composant anonyme :

Avec ce code :

@props(['post'])

<div class="s-hero__slide">

  <div class="s-hero__slide-bg" style="background-image: url('storage/photos/{{ $post->user->id }}/{{ $post->image }}')"></div>

  <div class="row s-hero__slide-content animate-this">
      <div class="column">
          <div class="s-hero__slide-meta">
              <span class="cat-links">
                  @foreach($post->categories as $category)
                      <a href="#">{{ $category->title }}</a>
                  @endforeach
              </span>
              <span class="byline"> 
                  @lang('Posted By') 
                  <span class="author">
                      <a href="#">{{ $post->user->name }}</a>
                  </span>
              </span>
          </div>
          <h1 class="s-hero__slide-text">
              <a href="#">{{ $post->title }}</a>
          </h1>
      </div>
  </div>

</div>

Pour l’instant on ne peut pas renseigner les liens mais ça viendra…

Les heros dans la vue

On peut maintenant intégrer les heros dans la vue index :

@section('hero')

    @isset($heros)

        <section id="hero" class="s-hero">

          <div class="s-hero__slider">

              @foreach($heros as $hero)
                  <x-front.hero :post="$hero" />
              @endforeach

          </div>

          <div class="s-hero__social hide-on-mobile-small">
              <p>@lang('Follow')</p>
              <span></span>
              <ul class="s-hero__social-icons">
                  <li><a href="#0"><i class="fab fa-facebook-f" aria-hidden="true"></i></a></li>
                  <li><a href="#0"><i class="fab fa-twitter" aria-hidden="true"></i></a></li>
                  <li><a href="#0"><i class="fab fa-instagram" aria-hidden="true"></i></a></li>
                  <li><a href="#0"><i class="fab fa-dribbble" aria-hidden="true"></i></a></li>
              </ul>
          </div>

          <div class="nav-arrows s-hero__nav-arrows">
              <button class="s-hero__arrow-prev">
                  <svg viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M1.5 7.5l4-4m-4 4l4 4m-4-4H14" stroke="currentColor"></path></svg>
              </button>
              <button class="s-hero__arrow-next">
                <svg viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M13.5 7.5l-4-4m4 4l-4 4m4-4H1" stroke="currentColor"></path></svg>
              </button>
          </div>

        </section>

    @endisset

@endsection

Tous les heros sont envoyés dans le composant qu’on a créé avec ce code :

@foreach($heros as $hero)
    <x-front.hero :post="$hero" />
@endforeach

Si vous avez bien travaillé ça doit fonctionner avec le diaporama :

Les articles

Un helper

Pour chaque article de la page d’accueil on va devoir aller récupérer l’image réduite (thumb). On va se créer un petit helper pour le faire. Comme on n’a pas encore de fichier pour les helpers on va en créer un :

Pour que les fonction qu’on va ajouter dans ce fichier soient connues il faut informer l’autoloader dans composer.json :

"autoload": {
    "psr-4": {
        ...
    },
    "files": [
        "app/helpers.php"
    ]
},

On crée la fonction pour aller chercher une image :

<?php

if (!function_exists('getImage')) {
    function getImage($post, $thumb = false)
    {   
        $url = "storage/photos/{$post->user->id}";
        if($thumb) $url .= '/thumbs';
        return asset("{$url}/{$post->image}");
    }
}

Pour que ce soit vraiment prise en compte il faut raffraîchir l’autoload :

composer dumpautoload

Les bricks

On va avoir des pavés (dans Calvin ça s’appelle des bricks). Là encore on va créer un composant parce que le code est répétitif :

Avec ce code :

@props(['post'])

<article class="brick entry" data-aos="fade-up">

  <div class="entry__thumb">
      <a href="#" class="thumb-link">
          <img src="{{ getImage($post, true) }}" alt="" style="width:100%">
      </a>
  </div>

  <div class="entry__text">
      <div class="entry__header">
          <h1 class="entry__title"><a href="#">{{ $post->title }}</a></h1>          
          <div class="entry__meta">
              <span class="byline"">@lang('By:')
                  <span class='author'>
                      <a href="#">{{ $post->user->name }}</a>
                  </span>
              </span>
          </div>
      </div>
      <div class="entry__excerpt">
          <p>{{ $post->excerpt }}</p>
      </div>
      <a class="entry__more-link" href="#">@lang('Read More')</a>
  </div>

</article>

Les bricks dans la vue

On peut maintenant intégrer les bricks dans la vue index :

@section('main')

    @isset($title)
        <div class="row">
            <div class="column">
                <h1>{!! $title !!}</h1>
            </div>
        </div>
    @endisset

    <div class="s-bricks">

      <div class="masonry">
          <div class="bricks-wrapper h-group">

              <div class="grid-sizer"></div>

              <div class="lines">
                  <span></span>
                  <span></span>
                  <span></span>
              </div>

              @foreach($posts as $post)

                  <x-front.brick :post="$post" />

              @endforeach

            </div>

      </div>

      <div class="row">
          <div class="column large-12">
              {{-- On va devoir s'occuper de la pagination ici --}}
          </div>
      </div>

  </div>

@endsection

La pagination ne fonctionnera pas encore mais vous devriez avoir les bricks :

La pagination

La pagination dans Laravel est prévue pour Bootstrap et Tailwind mais évidemment pas pour Calvin ! Alors on va devoir la créer…

Créez une nouvelle vue pour cette pagination :

Avec ce code :

@if ($paginator->hasPages())
    <nav class="pgn">
        <ul>
            {{-- Previous Page Link --}}
            <li>
                @if ($paginator->onFirstPage())
                    <span class="pgn__prev inactive" href="#0">@lang('Prev')</span>
                @else
                    <a href="{{ $paginator->previousPageUrl() }}" class="pgn__prev" rel="prev">@lang('Prev')</a>
                @endif
            </li>

            {{-- Pagination Elements --}}
            @foreach ($elements as $element)

                {{-- "Three Dots" Separator --}}
                @if (is_string($element))
                    <li><span class="pgn__num current">{{ $element }}</span></li>
                @endif

                {{-- Array Of Links --}}
                @if (is_array($element))
                    @foreach ($element as $page => $url)
                        <li>
                            @if ($page == $paginator->currentPage())
                                <span class="pgn__num current">{{ $page }}</span>
                            @else
                                <a href="{{ $url }}" class="pgn__num">{{ $page }}</a>
                            @endif
                        </li>
                    @endforeach
                @endif

            @endforeach

            {{-- Next Page Link --}}
            <li>
                @if ($paginator->hasMorePages())
                    <a href="{{ $paginator->nextPageUrl() }}"  class="pgn__next" rel="next">@lang('Next')</a>
                @else
                    <span class="pgn__next inactive">@lang('Next')</span>
                @endif
            </li>
        </ul>
    </nav>
@endif

Dans la vue index modifiez ainsi le code de la zone pour la pagination :

<div class="row">
    <div class="column large-12">
        {{ $posts->links('front.pagination') }}
    </div>
</div>

Et là si tout se passe bien :

On a enfin une page d’accueil qui fonctionne !

Les menu

Il est temps maintenant de s’occuper un peu du menu…

On va ajouter un helper (dans le fichier helpers) pour repérer la route active et placer la bonne classe :

if (!function_exists('currentRoute')) {
    function currentRoute($route)
    {
        return Route::currentRouteNamed($route) ? ' class=current' : '';
    }
}

Pour le menu on va se contenter pour le moment du lien pour la page d’accueil et du sous-menu des catégories. Je montre là tout le header :

<header class="s-header">

    <div class="s-header__logo">
        <a class="logo" href="{{ route('home') }}">
            <img src="{{ asset('images/logo.svg') }}" alt="Homepage">
        </a>
    </div>

    <div class="row s-header__navigation">

        <nav class="s-header__nav-wrap">

            <h3 class="s-header__nav-heading h6">@lang('Navigate to')</h3>

            <ul class="s-header__nav">
                <li {{ currentRoute('home') }}>
                    <a href="{{ route('home') }}" title="">@lang('Home')</a>
                </li>
                <li class="has-children">
                    <a href="#" title="">@lang('Categories')</a>
                    <ul class="sub-menu">
                        @foreach($categories as $category)
                            <li><a href="#">{{ $category->title }}</a></li>
                        @endforeach
                    </ul>
                </li>
            </ul>

            <a href="#0" title="@lang('Close Menu')" class="s-header__overlay-close close-mobile-menu">@lang('Close')</a>

        </nav>

    </div>

    <a class="s-header__toggle-menu" href="#0" title="@lang('Menu')"><span>@lang('Menu')</span></a>

    <div class="s-header__search">

        <div class="s-header__search-inner">
            <div class="row wide">

                <form role="search" method="get" class="s-header__search-form" action="#">
                    <label>
                        <span class="h-screen-reader-text">@lang('Search for:')</span>
                        <input type="search" class="s-header__search-field" placeholder="Search for..." value="" name="s" title="Search for:" autocomplete="off">
                    </label>
                    <input type="submit" class="s-header__search-submit" value="Search"> 
                </form>

                <a href="#0" title="@lang('Close Search')" class="s-header__overlay-close">@lang('Close')</a>

            </div>
        </div>

    </div>

    <a class="s-header__search-trigger" href="#">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.982 17.983"><path fill="#010101" d="M12.622 13.611l-.209.163A7.607 7.607 0 017.7 15.399C3.454 15.399 0 11.945 0 7.7 0 3.454 3.454 0 7.7 0c4.245 0 7.699 3.454 7.699 7.7a7.613 7.613 0 01-1.626 4.714l-.163.209 4.372 4.371-.989.989-4.371-4.372zM7.7 1.399a6.307 6.307 0 00-6.3 6.3A6.307 6.307 0 007.7 14c3.473 0 6.3-2.827 6.3-6.3a6.308 6.308 0 00-6.3-6.301z"/></svg>
    </a>

</header>

J’en ai profité pour localiser quelques textes.

On a maintenant un début de menu :

Conclusion

On a désormais une page d’accueil fonctionnelle, même s’il faudra encore y revenir pour plusieurs raisons (compléter le menu, ajouter des liens, adapter les liens sociaux, modifier le bas de page…).

Dans le prochain article on s’occupera de l’affichage des articles. On verra également l’affichage par catégorie, par auteur, par étiquette. On codera aussi la recherche.

Print Friendly, PDF & Email

44 commentaires

Laisser un commentaire