Drupal et les PWA - Part. 3 - Exemple d'une PWA avec Drupal 8 et Gatsby

Mardi 28 Mai 2019

Pour ce troisième article, nous allons montrer un petit exemple de mise en place d’une Progressive Web App (PWA) générée avec Drupal 8 et Gatsby.

PWA Drupal 8 et gatsby JS

Nous utiliserons la distribution Contenta qui active par défaut le module JSON:API (intégré au core depuis la version 8.7), mais ce n’est pas absolument nécessaire et un simple Drupal pourrait faire l’affaire.

Pour faire simple, nous utiliserons les contenus de démo installés avec la distribution et plus particulièrement le type de contenu Recipes.

Pré-requis

  • Connaître Drupal, la gestion du contenu et l’activation / ajout de modules ;
  • Avoir NodeJS (version 10+) installé sur sa machine ;
  • Connaître un peu React.

Drupal et Contenta

Nous ne nous attarderons pas ici sur l’installation de cette distribution, vous pouvez la faire en local, sur un serveur à vous ou chez Pantheon par exemple. Par défaut la liste des recettes est accessible à cette URL : http://ndd.tld/api/allRecipes

En voici un extrait :

{
  "jsonapi": {
    "version": "1.0",
    "meta": {
      "links": { "self": { "href": "http://jsonapi.org/format/1.0/" } }
    }
  },
  "data": [
    {
      "type": "recipes",
      "id": "b585c399-284f-443f-8440-64df2225ada6",
      "attributes": {
        "internalId": 66,
        "isPublished": true,
        "title": "La Trouchia (omelette aux blettes)",
        "createdAt": "2019-05-15T16:07:04+0200",
        "updatedAt": "2019-05-20T14:55:31+0200",
        "isPromoted": true,
        "revision_translation_affected": true,
        "path": null,
        "cookingTime": 10,
        "difficulty": "easy",
        "ingredients": [
          "6 oeufs",
          "1 kg de vert de blettes (sans les cotes)",
          "100g de parmesan r\u00e2p\u00e9",
          "75 de pignon",
          "huile d\u0027olive",
          "sel et poivre"
        ],
        "numberOfservings": 4,
        "preparationTime": 10,
        "instructions": "\u003Cp\u003E\u0026nbsp;\u003C/p\u003E\r\n\r\n\u003Cp\u003ERincer abondamment les verts de blettes, \u00e9goutter. Faire tomber dans un faitout , en remuant les blettes avec une fourchette piqu\u00e9e d\u2019une gousse d\u2019ail. A cuisson \u00e9goutter et presser entre les mains pour \u00e9liminer l\u2019humidit\u00e9.\u003C/p\u003E\r\n\r\n\u003Cp\u003EHacher la blette au couteau. Battre les \u0153ufs \u00e0 la fourchette, saler, poivrer, incorporer la blette, le parmesan. Ajouter les pignons juste torr\u00e9fi\u00e9s.\u003C/p\u003E\r\n\r\n\u003Ch3\u003ECuisson\u003C/h3\u003E\r\n\r\n\u003Cp\u003EDans une po\u00eale tenue chaude, verser trois bonnes cuill\u00e8res \u00e0 soupe d\u2019huile d\u2019olive, verser la pr\u00e9paration aux \u0153ufs.\u003C/p\u003E\r\n\r\n\u003Cp\u003E\u0026nbsp;\u003C/p\u003E\r\n\r\n\u003Cp\u003ELaisser cuire \u00e0 feu tr\u00e8s lent en veillant \u00e0 ce que l\u2019omelette n\u2019attache pas.\u003C/p\u003E\r\n\r\n\u003Cp\u003EPuis retourner sur une assiette, huiler encore un peu la po\u00eale et cuire l\u2019autre cot\u00e9.\u003C/p\u003E\r\n\r\n\u003Cp\u003EServir chaud ou ti\u00e8de, accompagn\u00e9 d\u2019une salade de mesclun.\u003C/p\u003E\r\n\r\n\u003Cp\u003EMais aussi emporter en pique nique, sur l\u2019herbe au festin des Mais\u003C/p\u003E\r\n\r\n\u003Cp\u003E\u0026nbsp;\u003C/p\u003E\r\n",
        "summary": {
          "value": "\u003Cp\u003ELa c\u00e9l\u00e8bre recette de l\u0027omelette ni\u00e7oise\u003C/p\u003E\r\n",
          "format": "basic_html",
          "processed": "\u003Cp\u003ELa c\u00e9l\u00e8bre recette de l\u0027omelette ni\u00e7oise\u003C/p\u003E"
        }
      },
      "relationships": {...},
      "links": {
        "self": {
          "href": "http://ndd.tld/api/allRecipes/b585c399-284f-443f-8440-64df2225ada6?resourceVersion=id%3A68"
        }
      }
    },

Drupal et GatsbyJS

Gatsby

C’est le moment de sortir son terminal et d’installer Gatsby !

npm install -g gatsby-cli

Quelques secondes plus tard, Gatsby est installé et nous pouvons commencer à créer notre premier site

gatsby new pwa3

Déplacez-vous dans ce répertoire et lancez la commande gatsby develop pour exécuter le serveur de développement.

Vous pouvez voir votre premier site Gatsby à l’adresse : http://localhost:8000/

Interface 1er site GatsbyJS

En parallèle, lancez votre éditeur de code favori pour faire un petit tour du propriétaire et intéressons nous en premier au répertoire src/pages qui contient quelques pages statiques.

La page d’accueil étant représentée par le fichier index.js.

La voici avec quelques petits changements :

import React from "react"
import { Link } from "gatsby"

import Layout from "../components/layout"
import Image from "../components/image"
import SEO from "../components/seo"

const IndexPage = () => (
  <Layout>
    <SEO title="Home" />
    <h1>Salut tout le monde</h1>
    <p>Bienvenue sur mon premier site Gatsby</p>
    <p>Cela manque un peu de recettes, vous ne trouvez pas ?</p>
    <div style={{ maxWidth: `300px`, marginBottom: `1.45rem` }}>
      <Image />
    </div>
    <Link to="/page-2/">Go to page 2</Link>
  </Layout>
)

export default IndexPage

Vous remarquerez que le code HTML important de cette page est inclus dans une balise <Layout> qui est un component React.

Il se trouve justement dans src/components !

import React from "react"
import PropTypes from "prop-types"
import { StaticQuery, graphql } from "gatsby"

import Header from "./header"
import "./layout.css"

const Layout = ({ children }) => (
  <StaticQuery
    query={graphql`
      query SiteTitleQuery {
        site {
          siteMetadata {
            title
          }
        }
      }
    `}
    render={data => (
      <>
        <Header siteTitle={data.site.siteMetadata.title} />
        <div
          style={{
            margin: `0 auto`,
            maxWidth: 960,
            padding: `0px 1.0875rem 1.45rem`,
            paddingTop: 0,
          }}
        >
          <main>{children}</main>
          <footer>
            © {new Date().getFullYear()}, Built with
            {` `}
            <a href="https://www.gatsbyjs.org">Gatsby</a>
          </footer>
        </div>
      </>
    )}
  />
)

Layout.propTypes = {
  children: PropTypes.node.isRequired,
}

export default Layout

On retrouve <main>{children}</main> qui correspond à l’emplacement prévu pour le code de notre page d’accueil.

On retrouve également un autre composant <Header siteTitle={data.site.siteMetadata.title} /> qui prend en paramètre une étrange propriété (props dans la terminologie React) : {data.site.SiteMetadata.title}. D’où vient-elle ?

La réponse est un peu plus haut sur la page, il y a un composant <StaticQuery> qui exécute une requête GraphQL pour récupérer ces fameuses données. Le titre du Header est donc extrait à partir d’une requête.

La source de ce titre se trouve dans src/gatsby-config.js :

module.exports = {
  siteMetadata: {
    title: `Gatsby Default Starter`,
    description: `Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
    author: `Nous-mêmes`,
  },

Vous pouvez faire quelques changements et à la sauvegarde du fichier, notez comme le rafraichissement de la page est automatique, merci Gatsby !

Tout ceci semble un peux obscur mais heureusement Gatsby nous fournit également une interface pour tester nos requêtes GraphQL à cette adresse :

http://localhost:8000/_graphql

Dans la colonne de gauche, copier / coller cette requête (qui correspond à notre StaticQuery et appuyez sur play

query SiteTitleQuery {
  site {
    siteMetadata {
      title
    }
  }
}

Le voilà notre SiteMetada.title :

{
  "data": {
    "site": {
      "siteMetadata": {
        "title": "Un site de recettes"
      }
    }
  }
}

Vous remarquerez que le fichier gatsby-config.js contient d’autres valeurs comme une description et un auteur. Vous voulez les récupérer ? Voici la requête :

query SiteTitleQuery {
  site {
    siteMetadata {
      title,
      author,
      description
    }
  }
}

Notez au passage avec bonheur que l’interface vous offre de l’auto-complétion sur les données existantes.

{
  "data": {
    "site": {
      "siteMetadata": {
        "title": "Un site de recettes",
        "author": "@gatsbyjs",
        "description": "Kick off your next, great Gatsby project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need."
      }
    }
  }
}

Retournons dans notre fichier src/Layout.js et modifions la requête avec nos nouvelles données.

const Layout = ({ children }) => (
  <StaticQuery
    query={graphql`
    query SiteTitleQuery {
      site {
        siteMetadata {
          title
          author
          description
        }
      }
    }
    `}
    render={data => (
      <>
        <Header siteTitle={data.site.siteMetadata.title} />
        <div
          style={{
            margin: `0 auto`,
            maxWidth: 960,
            padding: `0px 1.0875rem 1.45rem`,
            paddingTop: 0,
          }}
        >
          <main>{children}</main>
          <footer>
            © {new Date().getFullYear()}, Built with
            {` `}
            <a href="https://www.gatsbyjs.org">{data.site.siteMetadata.author}</a>
          </footer>
        </div>
      </>
    )}
  />
)

N’est-ce pas magnifique ?

Drupal 8 et GatsbyJS

C’est sympa, mais il est où Drupal ?

Gatsby utilise GraphQL pour chercher les données à afficher, il nous faut donc un module pour pouvoir interroger l’API JSON de Contenta et récupérer nos recettes en GraphQL.

Heureusement il y a déjà un plugin pour cela.

Arrêtons notre serveur de développement et installons ce fameux module !

npm install --save gatsby-source-drupal

Après (encore…) quelques instants, le module est installé, mais il faut l’ajouter à gatsby-config.js, dans la section plugins.

{
  resolve: `gatsby-source-drupal`,
  options: {
    baseUrl: `http://ndd.tld/`,
    apiBase: `api`,
    },
},

Lançons de nouveau le serveur de dev avec gatsby develop et jouons avec l’interface graphQL.

Vous remarquerez que l’auto-complétion nous fournit de nouvelles sources !

{
  allRecipes {
    edges {
      node {
        title
      }
    }
  }
}

allRecipes nous donnant accès à tous les résultats, la requête précédente affiche le titre de l’ensemble des nodes de ce type de contenu.

{
  "data": {
    "allRecipes": {
      "edges": [
        {
          "node": {
            "title": "La Trouchia (omelette aux blettes)"
          }
        },
        {
          "node": {
            "title": "Fiery chili sauce"
          }
        },
        {
          "node": {
            "title": "Crema catalana"
          }
        }
      ]
    }
  }
}

Victoire ! Mais comment faire pour utiliser cela dans Gatsby ?

Utilisons la page src/pages/page-2.js pour nos expérimentations et importons en premier lieu GraphQL en haut de page :

import { graphql } from "gatsby"

Puis créons une variable query avec notre requête Drupal avec quelques champs en plus (nombre de couverts et instructions) !

import React from "react"
import { Link } from "gatsby"
import { graphql } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"

export const query = graphql`
  query {
    allRecipes {
      edges {
        node {
          title,
          numberOfservings,
          instructions
        }
      }
    }
  }
`

const SecondPage = ({data}) => (
  <Layout>
    <SEO title="Page two" />
    <h1>{ data.allRecipes.edges[0].node.title }</h1>
    <p>Une recette pour { data.allRecipes.edges[0].node.numberOfservings} personne(s).</p>
    <h2>Instructions</h2>
    <div dangerouslySetInnerHTML={{ __html : data.allRecipes.edges[0].node.instructions }}></div>
    <Link to="/">Go back to the homepage</Link>
  </Layout>
)

export default SecondPage

C’est basique et ce n’est absolument pas dynamique dans le sens où j’ai spécifié le résultat dans la liste des recettes que je voulais ( edges[Ø]) mais cela fonctionne.

Drupal 8 et GatsbyJS

Nous utilisons une directive propre à Gatsby pour traiter l’HTML brut renvoyé par la requête :

<div dangerouslySetInnerHTML={{ __html: data.allRecipes.edges[0].node.instructions }}></div>

Générer le site

gatsby build

et le site statique généré se trouve dans le répertoire public.

Dans un prochain article nous verrons comment générer dynamiquement une page pour chaque noeud du type recette et une belle liste des dernières recettes.

Ludovic Coullet