Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Créer un blog - les articles
Lundi 1 février 2021 18:44

Nous avons dans le précédent article mis en place la page d'accueil avec la barre de navigation, le diaporama (heros), les résumés des articles (bricks), la pagination adaptée. Dans cet article nous allons maintenant voir une fonctionnalité importante : l'affichage des articles. Il faut afficher encore la barre de navigation mais elle va devoir être un peu adaptée. On doit aussi afficher l'image de l'article puis son contenu. Il faut aussi rappeler le nom de l'auteur, les catégories et les étiquettes. Enfin on doit renseigner les liens vers les articles précédents et suivants. On va en profiter par ajouter la possibilité d'obtenir les articles par catégorie, par auteur et par étiquette. Et pour finir on codera la recherche.

Pour les commentaires ça attendra un prochain article parce que c'est une partie un peu délicate.

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

Les avatars

Pour la gestion des avatars on va utiliser un package :

composer require creativeorange/gravatar ~1.0
On va utiliser la façade alors on la déclare dans app/config/app.php :
'aliases' => [
    ...
    'Gravatar' => Creativeorange\Gravatar\Facades\Gravatar::class,
];

On ne va pas publier la configuration parce que les valeurs par défaut conviennent.

Le repository

Au niveau du repository (PostRepository) on a besoin de récupérer :
  • un article à partir de son slug
  • l'article éventuel qui précède
  • l'article éventuel qui suit
Ce qui donne ce code :
public function getPostBySlug($slug)
{
    // Post for slug with user, tags and categories
    $post = Post::with(
                'user:id,name,email',
                'tags:id,tag,slug',
                'categories:title,slug'
            )
            ->withCount('validComments')
            ->whereSlug($slug)
            ->firstOrFail();

    // Previous post
    $post->previous = $this->getPreviousPost($post->id);

    // Next post
    $post->next = $this->getNextPost($post->id);

    return $post;
}

protected function getPreviousPost($id)
{
    return Post::select('title', 'slug')
                ->whereActive(true)
                ->latest('id')
                ->firstWhere('id', '<', $id);
}

protected function getNextPost($id)
{
    return Post::select('title', 'slug')
                ->whereActive(true)
                ->oldest('id')
                ->firstWhere('id', '>', $id);
}
Pour un article à afficher on collecte toutes les informations nécessaires :
  • toutes les colonnes de l'article
  • le nom et l'email de l'auteur
  • le slug et le nom des étiquettes
  • le slug et le nom des catégories associées
  • le compte des commentaires pour savoir s'il y en a
  • les éventuels articles précédents et suivants avec leur titre et leur slug

Le contrôleur et la route

Au niveau du repository (PostController) on a besoin d'une méthode :
public function show(Request $request, $slug)
{
    $post = $this->postRepository->getPostBySlug($slug);

    return view('front.post', compact('post'));
}
On ajoute la route :
Route::prefix('posts')->group(function () {
    Route::name('posts.display')->get('{slug}', [FrontPostController::class, 'show']);
});

J'ai créé un groupe parce qu'on aura d'autres routes avec ce préfixe.

Les helpers

On va ajouter un helper (dans app/helpers) pour formatter correctement les dates :
if (!function_exists('formatDate')) {
  function formatDate($date)
  {
      return ucfirst(utf8_encode ($date->formatLocalized('%d %B %Y')));
  }
}

La vue front.post

On crée une nouvelle vue : Avec ce code :
@extends('front.layout')

@section('main')

<!-- post
================================================== -->
<div class="row">
  <div class="column large-12">

      <article class="s-content__entry format-standard">

          <div class="s-content__media">
              <div class="s-content__post-thumb">
                  <img src="{{ getImage($post) }}" alt="" style="width:100%">
              </div>
          </div>

          <div class="s-content__entry-header">
              <h1 class="s-content__title s-content__title--post">{{ $post->title }}</h1>
          </div>

          <div class="s-content__primary">

              <div class="s-content__entry-content">

                  {!! $post->body !!}

              </div>

              <div class="s-content__entry-meta">

                  <div class="entry-author meta-blk">
                      <div class="author-avatar">
                          <img class="avatar" src="{{ Gravatar::get($post->user->email) }}" alt="">
                      </div>
                      <div class="byline">
                          <span class="bytext">@lang('Posted By')</span>
                          <a href="#0">{{ $post->user->name }}</a>
                      </div>
                  </div>

                  <div class="meta-bottom">
                      
                      <div class="entry-cat-links meta-blk">
                          <div class="cat-links">
                              <span>@lang('In')</span> 
                              @foreach ($post->categories as $category)
                                  <a href="#">{{ $category->title }}</a>
                              @endforeach
                          </div>

                          <span>@lang('On')</span>
                          {{ formatDate($post->created_at) }}
                      </div>

                      <div class="entry-tags meta-blk">
                          <span class="tagtext">@lang('Tags')</span>
                          @foreach($post->tags as $tag)
                              <a href="#">{{ $tag->tag }}</a>
                          @endforeach
                      </div>

                  </div>

              </div>

              <div class="s-content__pagenav">
                  @isset($post->previous)
                      <div class="prev-nav">
                          <a href="{{ route('posts.display', $post->previous->slug) }}" rel="prev">
                              <span>@lang('Previous')</span>
                              {{ $post->previous->title }}
                          </a>
                      </div>
                  @endisset
                  @isset($post->next)
                      <div class="next-nav">
                          <a href="{{ route('posts.display', $post->next->slug) }}" rel="next">
                              <span>@lang('Next One')</span>
                              {{ $post->next->title }}
                          </a>
                      </div>
                  @endisset
               </div>

          </div>
      </article>

  </div>
</div>

@endsection
Normalement un article doit s'afficher avec un url du style monblog.ext/posts/slug.

On a bien l'affichage de l'image mais on remarque un souci avec la barre de navigation :

On va s'en occuper dans le layout. Déjà on va rendre la barre opaque si on n'est pas sur la page d'accueil :

<!-- header
================================================== -->
<header class="s-header @unless(currentRoute('home')) s-header--opaque @endunless">
On va aussi prévoir un padding :
<!-- content
================================================== -->
<section class="s-content @if(currentRoute('home')) s-content--no-top-padding @endif">
Maintenant c'est mieux :

On vérifie qu'on a pour l'article l'avatar, le nom, la date, les catégories et les étiquettes :

On a aussi les liens qui fonctionnent vers les articles précédent et suivant :

Et évidemment le titre et le contenu :

Le SEO

Comme on a prévu des information de SEO pour les articles il faut intervenir sur la vue de layout pour les insérer :

<title>{{ isset($post) && $post->seo_title ? $post->seo_title :  config('app.name') }}</title>
<meta name="description" content="{{ isset($post) && $post->meta_description ? $post->meta_description : __(config('app.description')) }}">
<meta name="author" content="{{ isset($post) ? $post->user->name : __(config('app.author')) }}">
@if(isset($post) && $post->meta_keywords)
    <meta name="keywords" content="{{ $post->meta_keywords }}">
@endif
Pour le SEO de la page d'accueil j'ai juste mis les informations dans config.app :
/*
|--------------------------------------------------------------------------
| SEO
|--------------------------------------------------------------------------
*/

'description' => 'The best blog in the world',
'author' => 'The best author',

Les articles par catégorie

Dans le layout on a prévu précédemment les catégories dans le menu :

Pour le moment ça ne fonctionne pas. On commence par ajouter une fonction dans le repository (PostRepository) :

public function getActiveOrderByDateForCategory($nbrPages, $category_slug)
{
    return $this->queryActiveOrderByDate()
                ->whereHas('categories', function ($q) use ($category_slug) {
                    $q->where('categories.slug', $category_slug);
                })->paginate($nbrPages);
}
On crée aussi une fonction dans le contrôleur (PostController) :
use App\Models\Category;

...

public function category(Category $category)
{
    $posts = $this->postRepository->getActiveOrderByDateForCategory($this->nbrPages, $category->slug);
    $title = __('Posts for category ') . '<strong>' . $category->title . '</strong>';

    return view('front.index', compact('posts', 'title'));
}
On crée une route :
Route::name('category')->get('category/{category:slug}', [FrontPostController::class, 'category']);
On ajoute les liens dans le menu du layout :
<li class="has-children">
    <a href="#" title="">@lang('Categories')</a>
    <ul class="sub-menu">
        @foreach($categories as $category)
            <li><a href="{{ route('category', $category->slug) }}">{{ $category->title }}</a></li>
        @endforeach
    </ul>
</li>

Maintenant quand on utilise le menu en choisissant une catégorie on obtient uniquement les articles de cette catégorie :

Il faut aussi ajouter le lien dans la page de l'article (views/front/post) :
<div class="cat-links">
    <span>@lang('In')</span> 
    @foreach ($post->categories as $category)
        <a href="{{ route('category', $category->slug) }}">{{ $category->title }}</a>
    @endforeach
</div>

Les articles par auteur

Quand on clique sur le nom d'un auteur au niveau des bricks ou dans un article on doit obtenir les articles de cet auteur. On commence par ajouter une fonction dans le repository (PostRepository) :

public function getActiveOrderByDateForUser($nbrPages, $user_id)
{
    return $this->queryActiveOrderByDate()
                ->whereHas('user', function ($q) use ($user_id) {
                    $q->where('users.id', $user_id);
                })->paginate($nbrPages);
}
On crée aussi une fonction dans le contrôleur (PostController) :
use App\Models\{ Category, User };

...

public function user(User $user)
{
    $posts = $this->postRepository->getActiveOrderByDateForUser($this->nbrPages, $user->id);
    $title = __('Posts for author ') . '<strong>' . $user->name . '</strong>';

    return view('front.index', compact('posts', 'title'));
}
On crée une route :
Route::name('author')->get('author/{user}', [FrontPostController::class, 'user']);
On renseigne les liens dans les bricks (views/components/front/brick.blade.php) et on en profite pour ajouter les liens vers l'article :
@props(['post'])

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

  <div class="entry__thumb">
      <a href="{{ route('posts.display', $post->slug) }}" 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="{{ route('posts.display', $post->slug) }}">{{ $post->title }}</a></h1>          
          <div class="entry__meta">
              <span class="byline"">@lang('By:')
                  <span class='author'>
                      <a href="{{ route('author', $post->user->id) }}">{{ $post->user->name }}</a>
                  </span>
              </span>
          </div>
      </div>
      <div class="entry__excerpt">
          <p>{{ $post->excerpt }}</p>
      </div>
      <a class="entry__more-link" href="{{ route('posts.display', $post->slug) }}">@lang('Read More')</a>
  </div>

</article>

Maintenant on a un accès à l'article et on peut obtenir les articles par auteur :

Il faut aussi ajouter le lien dans la page de l'article (views/front/post) :

<div class="byline">
    <span class="bytext">@lang('Posted By')</span>
    <a href="{{ route('author', $post->user->id) }}">{{ $post->user->name }}</a>
</div>

Les articles par étiquette

Quand on clique sur le nom d'une étiquette dans un article on doit obtenir les articles pour cette étiquette. On commence par ajouter une fonction dans le repository (PostRepository) :

public function getActiveOrderByDateForTag($nbrPages, $tag_slug)
{
    return $this->queryActiveOrderByDate()
                ->whereHas('tags', function ($q) use ($tag_slug) {
                    $q->where('tags.slug', $tag_slug);
                })->paginate($nbrPages);
}
On crée aussi une fonction dans le contrôleur (PostController) :
use App\Models\{ Category, User, Tag };

...

public function tag(Tag $tag)
{
    $posts = $this->postRepository->getActiveOrderByDateForTag($this->nbrPages, $tag->slug);
    $title = __('Posts for tag ') . '<strong>' . $tag->tag . '</strong>';

    return view('front.index', compact('posts', 'title'));
}
On crée une route :
Route::name('tag')->get('tag/{tag:slug}', [FrontPostController::class, 'tag']);
Il faut aussi ajouter les liens dans la page de l'article (views/front/post) :
<div class="entry-tags meta-blk">
    <span class="tagtext">@lang('Tags')</span>
    @foreach($post->tags as $tag)
        <a href="{{ route('tag', $tag->slug) }}">{{ $tag->tag }}</a>
    @endforeach
</div>
Maintenant on peut obtenir les articles par étiquette :

La recherche

Notre blog est équipé d'un formulaire de recherche :

On va s'occuper de cette fonction. Les recherches porteront sur le titre, le résumé (excerpt) et le corps (body).

Le repository

On ajoute une fonction dans le repository (PostRepository) :

public function search($n, $search)
{
    return $this->queryActiveOrderByDate()
                ->where(function ($q) use ($search) {
                    $q->where('excerpt', 'like', "%$search%")
                      ->orWhere('body', 'like', "%$search%")
                      ->orWhere('title', 'like', "%$search%");
                })->paginate($n);
}

La validation

Pour la validation on crée une form request :
php artisan make:request Front/SearchRequest
<?php

namespace App\Http\Requests\Front;

use Illuminate\Foundation\Http\FormRequest;

class SearchRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'search' => 'required|string|max:100',
        ];
    }
}

Le contrôleur et la route

Dans le contrôleur PostController on ajoute une fonction :
use App\Http\Requests\Front\SearchRequest;

...

public function search(SearchRequest $request)
{
    $search = $request->search;
    $posts = $this->postRepository->search($this->nbrPages, $search);
    $title = __('Posts found with search: ') . '<strong>' . $search . '</strong>';

    return view('front.index', compact('posts', 'title'));
}
Et on crée la route :
Route::prefix('posts')->group(function () {
    ...
    Route::name('posts.search')->get('', [FrontPostController::class, 'search']);
});

Le layout

On ajoute l'action dans le formulaire au niveau du layout :
<form role="search" method="get" class="s-header__search-form" action="{{ Route('posts.search') }}">
    <label>
        <span class="h-screen-reader-text">@lang('Search for:')</span>
        <input id="search" type="search" name="search" class="s-header__search-field" placeholder="@lang('Search for...')" title="@lang('Search for:')" autocomplete="off">
    </label>
    <input type="submit" class="s-header__search-submit" value="Search"> 
</form>
Et maintenant la recherche fonctionne :  

Conclusion

On a maintenant un blog qui commence à avoir de l'allure ! Dans le prochain article on se penchera sur les commentaires. On va y passer un peu de temps parce que c'est un peu délicat et il va falloir pas mal de codage côté client et des requêtes en Ajax.



Par bestmomo

Nombre de commentaires : 30