Automatiser facilement la synchronisation de votre back-end

Fait partie de la série Améliorer votre blog VitePress avec une API Laravel

À la fin de l'article précédent, nous avons créé une table posts avec une colonne meta_id. Nous ne l'avons pas encore utilisée, cet article va donc la relier au front-end.

VitePress génère un site statique au moment du build, tandis que les commentaires sont chargés dynamiquement pour l'article actuellement consulté. Pour que cela fonctionne, le front-end et le back-end doivent disposer d'un moyen stable d'identifier le même article.

Par exemple, le blog garabit possède deux articles avec les slugs suivants :

  1. Getting Started with Laravel (getting-started-with-laravel)
  2. Getting Started with Vue 3 and Vite (getting-started-with-vue-3-and-vite)

Nous pourrions utiliser le slug comme identifiant, mais les slugs changent parfois à cause d'une faute de frappe, du référencement ou d'une évolution du contenu. Conserver un ancien slug uniquement pour préserver un identifiant rend les URL moins utiles. Nous avons besoin d'un identifiant distinct, qui reste stable même lorsque le slug change.

La solution consiste à stocker un identifiant stable dans le frontmatter de chaque article. Cet identifiant représente l'article et ne change pas lorsque son titre, son contenu ou son slug évolue.

Il reste à savoir comment envoyer ces identifiants au back-end. Une synchronisation manuelle est source d'erreurs et facile à automatiser ; le front-end va donc générer un fichier meta.json au moment du build. Il contiendra l'identifiant et le titre de chaque article. Le back-end lira ce fichier et créera ou mettra à jour les enregistrements correspondants dans la base de données. Le front-end pourra ensuite récupérer les commentaires en utilisant l'identifiant du frontmatter, tandis que la colonne posts.id restera une clé interne servant à relier les articles aux commentaires.

L'architecture est simple : VitePress écrit les métadonnées des articles et Laravel les lit pour garder ses enregistrements synchronisés.

Générer le fichier meta.json

Pour cette partie, ouvrez le projet garabit. Nous allons créer une fonction genMeta qui lit tous les fichiers Markdown, extrait leur identifiant et leur titre depuis le frontmatter, puis les écrit dans dist/meta.json grâce au hook buildEnd de VitePress. VitePress appelle ce hook une fois le reste du build terminé, ce qui en fait un bon endroit pour générer le fichier.

Commencez par créer un dossier generators dans .vitepress. Il contiendra les fonctions appelées par le hook buildEnd.

Note

generators n'est qu'un choix de nom. Vous pouvez utiliser celui qui vous convient.

bash
mkdir .vitepress/generators

Créez meta.ts dans ce dossier. Il contiendra genMeta.

bash
touch .vitepress/generators/meta.ts

Écrivons enfin la fonction :

ts
import type { ContentData, SiteConfig } from 'vitepress'
import { writeFileSync } from 'node:fs'
import path from 'node:path'
import { createContentLoader } from 'vitepress'

export async function genMeta(config: SiteConfig) {
  const posts = await createContentLoader('**/blog/**/*.md').load()

  validatePosts(posts)

  const meta = []

  for (const item of posts) {
    meta.push({
      id: item.frontmatter.id,
      title: item.frontmatter.title,
    })
  }

  writeFileSync(path.join(config.outDir, 'meta.json'), JSON.stringify(meta.sort((a, b) => a.id - b.id), null, 2))
}

function validatePosts(items: ContentData[]) {
  const ids = new Set<string>()

  for (const item of items) {
    if (!item.frontmatter.id) {
      throw new Error(`Id is missing: ${item.url}`)
    }

    if (ids.has(item.frontmatter.id)) {
      throw new Error(`Id is not unique: ${item.frontmatter.id} (${item.url}) (${items.find(i => i.frontmatter.id === item.frontmatter.id)?.url})`)
    }

    ids.add(item.frontmatter.id)
  }
}

Le script se déroule en trois étapes. D'abord, createContentLoader lit les fichiers Markdown et analyse leur frontmatter. Ensuite, nous vérifions que chaque article possède un identifiant et que les identifiants sont uniques. C'est important : un article sans identifiant ne peut pas participer au système de commentaires, tandis que deux identifiants identiques feraient partager leurs commentaires à deux articles. Dans les deux cas, la validation lève une erreur et arrête le build.

Nous créons ensuite un tableau contenant l'identifiant et le titre de chaque article, puis nous l'écrivons dans dist/meta.json.

Pour l'exécuter, ajoutez genMeta au hook buildEnd dans .vitepress/config.ts :

ts
import { defineConfig } from 'vitepress'
import { genMeta } from './generators/meta'

export default defineConfig({
  // ...

  buildEnd: async (config) => {
    await genMeta(config)
  },
})

Ajoutez enfin un identifiant unique à chaque article :

md
---
id: 1 [!code ++]
title: Getting Started with Laravel
description:
date: 2024-10-14
---
md
---
id: 2
title: Getting Started with Vue 3 and Vite
description:
date: 2024-09-14
---

Lorsque nous construirons le front-end, VitePress créera meta.json dans dist. Le fichier contiendra l'identifiant et le titre de chaque article. Vérifiez-le avec :

bash
npm run build
bash
cat .vitepress/dist/meta.json

La sortie devrait ressembler à ceci :

json
[
  {
    "id": 1,
    "title": "Getting Started with Laravel"
  },
  {
    "id": 2,
    "title": "Getting Started with Vue 3 and Vite"
  }
]

Si vous voyez ce fichier, la partie front-end de la synchronisation est terminée.

Récupérer les articles depuis le back-end

Le back-end doit maintenant lire ce fichier et synchroniser la base de données. Nous allons créer une commande app:posts-sync qui récupère et analyse meta.json, puis insère ou met à jour chaque article.

Note

La colonne title n'est utilisée ni par le front-end ni par l'API, mais elle facilite l'identification des enregistrements en base et peut être utile dans un panneau d'administration.

Créez la commande avec Artisan :

bash
php artisan make:command PostsSync

La commande sera créée dans app/Console/Commands. Son rôle est simple :

  1. Récupérer le fichier meta.json depuis le front-end
  2. Insérer ou mettre à jour chaque article dans la base de données

Voici son implémentation :

php
<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Post;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;

final class PostsSync extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:posts-sync';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Synchronize backend posts with the frontend through the `meta.json` file.';

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        /**
         * @var array<array<string, int|string>>
         */
        $response = Http::get(Config::get('app.frontend_url').'/meta.json')
            ->json();

        $meta = collect($response)->map(
            fn (array $post): array => [
                'meta_id' => $post['id'],
                'title' => $post['title'],
            ]
        )->toArray();

        Post::upsert($meta, uniqueBy: ['meta_id'], update: ['title']);
    }
}

La méthode upsert gère les deux cas. Elle insère les enregistrements inexistants en utilisant la clé unique meta_id, et met à jour le titre d'un enregistrement existant.

La commande doit fonctionner dans les environnements local et de production. En local, le front-end peut être servi depuis localhost:5173 ou localhost:4173 pendant une prévisualisation. En production, il est servi depuis une URL publique. Nous gérons cette différence avec la variable d'environnement APP_FRONTEND_URL, exposée sous la clé frontend_url dans config/app.php.

ini
APP_FRONTEND_URL=http://localhost:4173
php
return [
    // ...

    'frontend_url' => env('APP_FRONTEND_URL', 'http://localhost:4173'),

    // ...
];

Le code est alors identique dans les deux environnements ; seule la variable d'environnement change.

En développement, démarrez le serveur de prévisualisation du front-end :

bash
npm run build && npm run preview

Lancez ensuite la commande de synchronisation :

bash
php artisan app:posts-sync

Note

Définissez la bonne valeur de APP_FRONTEND_URL dans .env avant de lancer la commande.

Si vous voyez SQLSTATE[HY000]: General error: 1 no such table: posts, la table posts n'a pas encore été créée. Exécutez :

bash
php artisan migrate

Vous pouvez maintenant lancer la commande de synchronisation autant de fois que nécessaire sans créer de doublons.

Tester la commande

Cette commande fait partie de notre application, nous devons donc la tester. Pour cela, nous allons simuler la façade Http. Nous pouvons ainsi tester la commande sans dépendre du réseau. C'est l'un des rares cas où un mock est utile.

Pour simuler le client HTTP, utilisez Http::fake. Il permet au test de fournir une réponse meta.json prévisible sans dépendre du réseau.

php
<?php

declare(strict_types=1);

use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;

it('fetches posts meta', function () {
    Http::fake([
        Config::get('app.frontend_url').'/meta.json' => Http::response([
            [
                'id' => 1,
                'title' => 'Post 1',
            ],
            [
                'id' => 2,
                'title' => 'Post 2',
            ],
        ], 200),
    ]);

    $this->artisan('app:posts-sync')
        ->assertExitCode(0);

    Http::assertSentCount(1);

    $this->assertDatabaseCount('posts', 2);
    $this->assertDatabaseHas('posts', [
        'meta_id' => 1,
        'title' => 'Post 1',
    ]);
});

it('does not insert duplicates', function () {
    Http::fake([
        Config::get('app.frontend_url').'/meta.json' => Http::response([
            [
                'id' => 1,
                'title' => 'Post 1',
            ],
            [
                'id' => 1,
                'title' => 'Post 1',
            ],
        ], 200),
    ]);

    $this->artisan('app:posts-sync')
        ->assertExitCode(0);

    Http::assertSentCount(1);

    $this->assertDatabaseCount('posts', 1);
    $this->assertDatabaseHas('posts', [
        'meta_id' => 1,
        'title' => 'Post 1',
    ]);
});

it('updates the data based on the id', function () {
    Http::fake([
        Config::get('app.frontend_url').'/meta.json' => Http::response([
            [
                'id' => 1,
                'title' => 'Post 1',
            ],
            [
                'id' => 1,
                'title' => 'Post 1 Updated',
            ],
        ], 200),
    ]);

    $this->artisan('app:posts-sync')
        ->assertExitCode(0);

    Http::assertSentCount(1);

    $this->assertDatabaseCount('posts', 1);
    $this->assertDatabaseHas('posts', [
        'meta_id' => 1,
        'title' => 'Post 1 Updated',
    ]);
});

Les tests de la commande se trouvent dans un nouveau dossier Console. Ajoutez ce dossier à phpunit.xml pour que PHPUnit les découvre :

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
    <testsuites>
        <testsuite name="Console"> 
            <directory>tests/Console</directory> 
        </testsuite> 
    </testsuites>
</phpunit>

Comme ces tests accèdent à la base de données, mettez à jour Pest afin qu'il la réinitialise avant chaque test :

php
pest()->extend(Tests\TestCase::class)
    ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Console', 'Http', 'Unit'); // [!code highlighter]

Derniers mots

Le front-end et le back-end partagent maintenant un identifiant stable pour chaque article. VitePress génère les métadonnées et Laravel peut synchroniser ses enregistrements sans mise à jour manuelle de la base de données.

Nous disposons maintenant du pont nécessaire aux fonctionnalités interactives. Ensuite, nous configurerons Sanctum, CORS et la protection CSRF, puis nous enverrons les premières requêtes de VitePress vers Laravel.

Pd

Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.

Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.

Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.

J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !

Continuer la lectureConnecter le front-end, VitePress et le back-end, Laravel

Réactions

Discussions

Ajouter un commentaire

Vous devez être connecté pour accéder à cette fonctionnalité.