2 septembre 2021
Gestion de blocks CMS avec Strapi, Next.js et TypeScript
6 minutes de lecture
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 :
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)
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
:
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 :
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.