Gestion de blocks CMS avec Strapi, Next.js et TypeScript

par

Baptiste

Baptiste Adrien

6 minutes de lecture

Gestion de blocks CMS avec Strapi, Next.js et TypeScript

Nous vous présentions dans notre dernier article, le CMS headless Strapi que nous avions eu l'occasion d'utiliser dans le cadre d'un projet. Aujourd'hui nous allons rentrer plus en détails, avec une fonctionnalité très pratique de Strapi : les composants et les zones dynamiques. Ceux-ci vont nous permettre de créer un système de pages CMS avec une gestion de blocks flexible.

Voici les différentes étapes dans la mise en place de notre système de CMS :

Côté back (Strapi):

  • Création de la structure des blocks dans Strapi (ex: Cover, Slider, Latest posts) ;
  • Création d'un système de pages CMS dans Strapi.

Coté front (Next.js)

  • Génération des types et client d'API TypeScript côté Next.js ;
  • Implémentation du routing CMS (génération statique) ;
  • Implémentation des blocks (avec TypeScript).

Définition des pages et blocks côté Strapi

Création de la structure des blocks

Nous allons commencer par créer nos différents blocks de notre CMS, par blocks on entend des widgets que l'utilisateur pourra ajouter à une page du CMS. Pour cela nous allons utiliser les composants de Strapi. Ceux-ci permettent de créer des types contenant plusieurs champs (image, texte, relations…) pouvant être réutilisés facilement.

Dans cet article nous allons créer deux composants :

  • Hero
  • Section

Pour cela cliquer sur Content Types builder dans le menu latéral puis Composant > Créer un composant.

Voici nos deux composants créés :

Composant Hero

Composant Section

Ajout du système de pages CMS

Ajoutons désormais notre gestion des pages dans Strapi. Pour cela nous allons créer une nouvelle collection de Page avec 3 champs :

  • title (texte)
  • description (texte)
  • slug (UID basé sur le title)

Collection Page

Enfin ajoutons un 4ème et dernier champ nommé blocks de type Dynamic Zone nous permettant d'ajouter nos blocks précédemment créés. Lors de la création de ce champ, nous devons définir les blocks qu'une Page peut afficher, dans notre cas les blocks Hero et Section :

Block d'une page

Il ne nous reste plus qu'à créer une page et y ajouter un ou plusieurs blocks :

Une fois sauvegardée, notre page est accessible via l'API REST générée par Strapi sur le endpoint http://localhost:1337/pages :

Pensez à autoriser le listing des pages (Paramètres > Roles & permissions > Public > Pages > find & findone)

Notre page contient bien un tableau blocks avec nos blocks ajoutés dans Strapi :

// http://localhost:1337/pages
[
  {
    "id": 1,
    "title": "Notre restaurant",
    "description": "Découvrez notre restaurant",
    "slug": "decouvrez-le-projet",
    "blocks": [
      {
        "__component": "block.hero",
        "id": 1,
        "title": "Notre restaurant",
        "image": {
          "id": 1,
          "width": 934,
          "height": 1401,
          "url": "/uploads/photo_1520279406162_c955e67194ed_6469e0ca7e.webp"
        }
      }
    ]
  }
]

Notre API est prête à être consommée par un front. Nous allons utiliser Next.js qui va nous permettre de générer statiquement nos pages CMS.

Implémentation du CMS côté front

Génération des types et client d’API en TypeScript

Chez Premier Octet nous développons tous nos projets React en TypeScript, nous permettant d'avoir une base de code solide et à l'épreuve de régression.

L'avantage de Strapi est d'exposer automatiquement une spécification Swagger exhaustive de l'API REST.

Activer la documentation en allant dans le menu Marketplaces > Documentation > Télécharger

En s'appuyant sur cette spécification, nous pouvons générer en une ligne de commande un client d'API Axios typé automatiquement grâce à la librairie swagger-typescript-api :

swagger-typescript-api -p ../server/extensions/documentation/documentation/1.0.0/full_documentation.json -o ./typings -n api.ts --route-types --module-name-index 0 --axios

Voici le type généré pour notre page :

/* eslint-disable */
/* tslint:disable */
/*
 * ---------------------------------------------------------------
 * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API        ##
 * ##                                                           ##
 * ## AUTHOR: acacode                                           ##
 * ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
 * ---------------------------------------------------------------
 */

export interface Page {
  id: string;
  title: string;
  description?: string;
  slug: uid;
  blocks?: (
    | {
        __component?: 'block.hero' | 'block.section';
        id: string;
        title: string;
        image?: {
          id: string;
          name: string;
          alternativeText?: string;
          caption?: string;
          width?: number;
          height?: number;
          formats?: object;
          hash: string;
          ext?: string;
          mime: string;
          size: number;
          url: string;
          previewUrl?: string;
          provider: string;
          provider_metadata?: object;
          related?: string;
          created_by?: string;
          updated_by?: string;
        };
      }
    | {
        __component?: 'block.hero' | 'block.section';
        id: string;
        title: string;
        buttonLabel: string;
        buttonUrl: string;
        description: string;
        image?: {
          id: string;
          name: string;
          alternativeText?: string;
          caption?: string;
          width?: number;
          height?: number;
          formats?: object;
          hash: string;
          ext?: string;
          mime: string;
          size: number;
          url: string;
          previewUrl?: string;
          provider: string;
          provider_metadata?: object;
          related?: string;
          created_by?: string;
          updated_by?: string;
        };
        imagePosition: 'left' | 'right';
      })[];

  /** @format date-time */
  published_at?: string;
}

Il y a deux petits soucis, le premier concerne le champs slug typé en uid dont la spécification swagger ne définie pas le type et qui est donc inconnu. Pour corriger cela, ajoutez un fichier nommé types.d.ts afin de définir globalement ce type (une string ici) :

type uid = string;

Le deuxième problème concerne le typage de la propriété __component de nos blocks, cela devrait être :

export interface Page {
  // ...
  blocks?: (
    | {
-        __component?: "block.hero" | "block.section";
+        __component?: "block.hero";
        //...
        };
      }
    | {
-      __component?: "block.hero" | "block.section";
+      __component?:  "block.section";
      //...
      }
  )[];
}

Nous avons corrigé ce problème par l'intermédiaire d'une PR sur Strapi mais celle-ci est toujours en attende de review : https://github.com/strapi/strapi/pull/10595. En attendant que celle-ci soit mergée dans Strapi vous pouvez éditer votre type ou bien appliquer un patch sur votre dépendance Strapi.

Nous pouvons désormais implémenter le routing dans Next.js.

Implémentation du routing CMS (génération statique)

Nous allons créer un fichier [pageName].tsx dans le dossier pages de Next.js (plus d'infos sur le routing de Next.js dans notre article dédié).

L'utilisation de la syntaxe [pageName] nous permet de créer une route "wildcard" pour afficher nos pages créées dynamiquement depuis Strapi. Nous allons demander à Next.js de créer nos pages de manière statique (au build puis de manière incrémentale) grâce à l'utilisation des deux méthodes getStaticPaths et getStaticProps :

// pages/[pageName].tsx

import { GetStaticProps, NextPage } from "next"
import { Api, Page } from "../typings/api"
import BlockRenderer from "../components/BlockRenderer"

const client = new Api()

const CmsPage: NextPage<{ page: Page }> = ({ page }) => {
  return <BlockRenderer blocks={page.blocks}  />
}

export async function getStaticPaths() {
  const { data: pages } = await client.pages.pagesList()

  const paths = pages.map((page) => ({
    params: {
      pageName: page.slug,
    },
  }))

  return {
    paths,
    fallback: 'blocking',
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const pageName = params!.pageName! as string

  try {
    const { data: page } = await client.pages.pagesDetail(pageName)

    return {
      props: {
        page,
      },
      revalidate: 60,
    }
  } catch (e) {
    return {
      notFound: true,
    }
  }
}

export default CmsPage
Par défaut l'API de Strapi utilise l'id dans l'url, vous pouvez utiliser le slug en suivant ce guide

Nous retournons dans la méthode getStaticPaths tous les chemins de nos pages CMS afin que Next.js puisse les compiler lors du build. Enfin nous récupérons les informations de la page (titre, blocks…) dans la méthode getStaticProps. Nous renseignons l'attribut revalidate à 60 afin de rafraîchir une page si celle-ci n'a pas été affichée depuis au moins 60 secondes.

Il ne nous reste plus qu'à implémenter le composant <BlockRenderer /> pour afficher les blocks :

import React from "react"
import { Page } from "../typings/api"

const BlockRenderer = ({ blocks }: { blocks: Page["blocks"] }) => {
  return (
    <>
      {blocks?.map((block) => {
        switch (block.__component) {
          case "block.hero":
            // Add the markup for the Hero block
            return <div />
          case "block.section":
            // Add the markup for the Section block
            return <div />
          default:
            null
        }
      })}
    </>
  )
}

export default BlockRenderer

Grâce à nos types, TypeScript infère bien sur le nom du block et propose uniquement ses propriétés :

Block Hero

Block Section

La combinaison de Strapi, TypeScript et Next.js offre donc un système élégant pour développer rapidement un système de CMS sans laisser de côté l'expérience développeur (typages TypeScript) et la performance grâce à la génération statique.

Continuer la discussion sur Twitter

18 avenue Parmentier
75011 Paris
+33 1 43 57 39 11
hello@premieroctet.com

Suivez nos aventures

GitHub

Naviguez à vue