Blog / Créer un blog avec Laravel, Vue et Canvas

Image used for article Créer un blog avec Laravel, Vue et Canvas

Créer un blog avec Laravel, Vue et Canvas




TL;DR: Comment créer rapidement un blog en utilisant le stack Vue Inertia Laravel Tailwind et l’outil Canvas d’Austin Todd.




Vous trouverez le code source via ce Github Repository. Découvrez en plus sur Capsules ou X.




Une petite précision concernant les langages et frameworks utilisés : Laravel gère le côté serveur, tandis que Vue prend en charge le côté client. Inertia sert de pont entre les deux. Pour l'aspect visuel du site, nous utilisons Tailwind CSS. Supposons que vous ayez un template Laravel Inertia Vue Tailwind prêt à l’emploi.



Canvas est un puissant outil pour les applications Laravel qui simplifie l'écriture, la modification et la personnalisation de votre contenu grâce à une gamme d'outils de publication. Il s'agit d'une solution tout-en-un incroyable pour la création et la publication d'articles, comme celui que vous êtes en train de lire.







Installation de Canvas via le terminal :


composer require austintoddj/canvas




Création d’une base de données blog via le terminal


mysql -u <username> -p <password> -e "CREATE DATABASE blog"




Configuration des données relatives à la base de données dans le fichier .env



.env


DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=
DB_PASSWORD=




Publication des ressources, du fichier de configuration principal, des migrations relatives à Canvas ainsi que du symlink su dossier storage.


php artisan canvas:install

php artisan storage:link




Canvas est désormais accessible à l’adresse suivante : http://your.domain.name/canvas Les identifiants générés lors de l'exécution de php artisan canvas:install permettent de se connecter au moyen du formulaire de connexion.



Il est désormais possible d'ajouter un article manuellement via l'outil Canvas. Un seeder a néanmoins été préalablement mis à disposition.



database/seeders/DatabaseSeeder.php


<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Canvas\Models\Post;
use Canvas\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Str;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $user = User::first();

        for( $amount = 1; $amount <= 6; $amount++ )
        {
            $post = new Post();

            $post->id = Str::uuid();
            $post->title = fake()->sentence();
            $post->slug = Str::snake( $post->title, '-' );
            $post->summary = fake()->paragraph();
            $post->body = '<p>' . fake()->paragraph( 2 ) . '</p><br><br><p>' . fake()->paragraph( 8 ) . '</p><br><br><p>' . fake()->paragraph( 6 ) . '</p>';
            $post->published_at = Carbon::now()->addSeconds( $amount );
            $post->featured_image = "/storage/canvas/images/capsules-blog-00{$amount}.jpg";
            $post->user()->associate( $user );

            $post->save();
        }
    }
}


Six images ont préalablement été glissées dans le dossier /canvas/images. Leur chemin d'accès est spécifié dans la propriété featured_image. Vous pouvez les retrouver dans le Github Repository mentionné précédemment.


La propriété body demande du code HTML.


Des secondes ont été ajoutées à la propriété published_at afin que les articles n'aient pas la même date de publication.




Le seeder peut être exécuté pour créer six articles en utilisant la commande suivante.


php artisan db:seed




Les articles sont prêts à être organisés et lus. Il est maintenant temps de les afficher. Pour commencer, la création d'un contrôleur PostControllerest nécessaire. Dans un premier temps, la méthode index listera les résumés des articles sur la page principale.



App/Http/Controllers/PostController.php


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Response;
use Canvas\Models\Post;
use Inertia\Inertia;
use App\Http\Resources\PostResource;

class PostController extends Controller
{
    public function index( Request $request ) : Response
    {
        $posts = Post::published()->orderBy( 'published_at', 'desc' )->get();

        return Inertia::render( 'Posts/Index', [ 'posts' => PostResource::collection( $posts )->toArray( $request ) ] );
    }
}


Il est nécessaire d'appeler la méthode toArray de la collection pour contourner la mise en place par défaut d'un objet JSON de pagination.




On sélectionne les articles publiés plutôt que les brouillons, on les trie en fonction de la date de publication la plus récente. Ces données sont ensuite injectées dans une ressource PostResource.



App/Resources/PostResource.php


<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Str;

class PostResource extends JsonResource
{
    public function toArray( Request $request ) : array
    {
        return [
            'title' => $this->title,
            'slug' => $this->slug,
            'summary' => $this->summary,
            'image' => $this->featured_image,
            'body' => $this->body,
            'time' => round( Str::wordCount( $this->body ) / 200 ),
            'date' => $this->published_at->format( 'd M Y' )
        ];
    }
}


Une petite particularité a été ajoutée à la ressource, à savoir la notion de time qui représente le temps de lecture de l'article. Ce temps est calculé en prenant le nombre de mots du body de l'article et en le divisant par un nombre arbitraire, soit 200. Basé sur l'hypothèse qu'un lecteur moyen lit 200 mots par minute en moyenne.




On peut dès lors créer la route principale dans le fichier web.php qui appellera alors la méthode index du PostController.



routes/web.php


use App\Http\Controllers\PostController;

Route::get( '/', [ PostController::class, 'index' ] )->name( 'posts.index' );




C'est maintenant au client et à un composant Vue de mettre en page cette liste d'articles dans Index.vue.



resources/js/pages/Posts/Index.vue


<script setup lang="ts">

import { Link } from '@inertiajs/vue3';

const props = defineProps( { posts : { type : Array, default : [] } } );

</script>

<template>

    <div class="mx-auto w-full min-h-screen max-w-screen-lg flex text-primary-black">

        <div class="my-16 mx-8 grid grid-cols-3 gap-x-8 gap-y-20">

            <article class="p-4 flex flex-col items-start first:col-span-3 justify-between rounded-xl bg-primary-white bg-opacity-75 hover:opacity-90 transition-opacity duration-300 ease-in-out" v-for=" ( post, index ) in props.posts " v-bind:key="post.slug">

                <div class="h-full flex flex-col" v-bind:class=" { 'grid grid-cols-3 gap-x-8' : index == 0 } ">

                    <Link v-bind:class=" { 'col-span-2 h-full' : index == 0 } " v-bind:href="`/${post.slug}`">

                        <img class="w-full aspect-video object-cover rounded-xl dark-image-mode" v-bind:class=" { 'h-full' : index == 0 } " v-bind:src="post.image">

                    </Link>

                    <div class="grow flex flex-col items-stretch justify-between">

                        <div v-bind:class=" { 'mt-4' : index != 0 } ">

                            <div v-bind:class=" { 'flex flex-col-reverse' : index == 0 } ">

                                <div class="mt-4 flex items-center space-x-4 text-xs">

                                    <p class="py-1" v-text="post.date" />

                                </div>

                                <Link v-bind:href="`/${post.slug}`"><h3 class="mt-4 text-lg font-bold" v-bind:class=" { 'text-2xl' : index == 0 }" v-text="post.title" /></Link>

                            </div>

                            <p class="mt-8 text-sm leading-6" v-bind:class=" index == 0 ? 'line-clamp-6' : 'line-clamp-3' " v-text="post.summary" />

                        </div>

                        <div class="mt-8 flex" v-bind:class=" index == 0 ? 'justify-start' : 'justify-end' ">

                            <Link class="px-4 py-2 rounded-md text-xs border" v-bind:href="`/${post.slug}`"><span v-text="'Read more'" /></Link>

                        </div>

                    </div>

                </div>

            </article>

        </div>

    </div>

</template>


Le premier article bénéficie d'un traitement spécial, et chaque élément de l'objet reçoit une classe Tailwind différente en fonction de l'indice index == 0.







Vient ensuite la page de lecture de l’article, nécessitant la création d’une nouvelle route.



routes/web.php


Route::get( '/{post:slug}', [ PostController::class, 'show' ] )->name( 'posts.show' );


L’indication {post:slug} permet d’utiliser le slug de l’article en URL au lieu de son id par défault.




Dès lors, l’implémentation de la méthode show peut être réalisée dans le PostController.



App/Http/Controlles/PostController.php


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Response;
use Canvas\Models\Post;
use Inertia\Inertia;
use App\Http\Resources\PostResource;
use Illuminate\Support\Facades\Event;
use Canvas\Events\PostViewed;

class PostController extends Controller
{
    public function index( Request $request ) : Response
    {
        $posts = Post::published()->orderBy( 'published_at', 'desc' )->get();

        return Inertia::render( 'Posts/Index', [ 'posts' => PostResource::collection( $posts )->toArray( $request ) ] );
    }

    public function show( Post $post ) : Response
    {
        Event::dispatch( new PostViewed( $post ) );

        return Inertia::render( 'Post/Show', [ 'post' => new PostResource( $post ) ] );
    }
}


L'outil Canvas dispose d'un tableau de bord qui est alimenté par une série d'événements, tels que celui ajouté dans la méthode show qui permet de comptabiliser le nombre de fois qu'un article a été visualisé en utilisant l'event dispatch Event::dispatch( new PostViewed( $post ) ).







La dernière étape consiste à créer la mise en page de la page Show.vue.



resources/js/pages/Posts/Show.vue


<script setup lang="ts">

import { Link } from '@inertiajs/vue3';

const props = defineProps( { post : { type : Object, required : true } } );

</script>

<template>

    <div class="mx-auto w-full min-h-screen max-w-screen-lg flex text-primary-black">

        <div class="m-8 sm:m-16">

            <Link class="m-8 flex items-center" v-bind:href="'/'">

                <p class="text-sm leading-6 truncate" v-text="`< Back to blog`" />

            </Link>

            <div class="mb-16 p-8 rounded-xl bg-primary-white">

                <div class="mb-16 flex items-stretch">

                    <img class="w-24 object-cover rounded-xl" v-bind:src="props.post.image">

                    <h1 class="ml-4 py-2 w-2/3 text-5xl font-bold" v-text="props.post.title" />

                </div>

                <div class="mt-4 flex items-center">

                    <p class="text-xs" v-text="`${props.post.time} min - ${props.post.date}`" />

                    <hr class="grow ml-4 border-solid">

                </div>

                <div class="mt-8 text-2xl font-extralight tracking-wide article" v-html="props.post.body" />

            </div>

        </div>

    </div>

</template>


Un bouton de retour semble approprié pour revenir à la page Posts/Index.







Tout est personnalisable grâce au travail de Todd Austin et de son outil Canvas qui a permis de créer ce blog en moins de temps qu’il a fallu pour écrire cet article.




Ravi d’avoir pu aider.

v1.3.1

Icône XIcône Github