- Part 1: PWA and Drupal, generalities
- Part 2: Decoupling
- Part 3: Example of a PWA with Drupal 8 and Gatsby
We’ll use the Contenta distribution, which by default enables the JSON:API module (integrated in the core since version 8.7), but this is not absolutely necessary and a plain Drupal could do the trick.
To keep things simple, we’ll use the demo content installed with the distribution, and more specifically the Recipes content type.
Prerequisites
- Know Drupal, content management, and how to enable/add modules;
- Have NodeJS (version 10+) installed on your machine;
- Be familiar with React.
Drupal and Contenta
We won’t dwell here on installing this distribution; you can set it up locally, on your own server, or for example with Pantheon. By default, the list of recipes is accessible at this URL: http://ndd.tld/api/allRecipes
Here’s an excerpt:
{
"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 eggs",
"1 kg chard greens (without the stalks)",
"100g grated parmesan",
"75g pine nuts",
"olive oil",
"salt and pepper"
],
"numberOfservings": 4,
"preparationTime": 10,
"instructions": "<p> </p>\r\n\r\n<p>Rinse the chard greens thoroughly, drain. Soften in a large pot, stirring the chard with a fork pierced with a garlic clove. When cooked, drain and squeeze out excess moisture.</p>\r\n\r\n<p>Chop the chard with a knife. Beat the eggs with a fork, salt, pepper, fold in the chard and parmesan. Add the pine nuts just after toasting.</p>\r\n\r\n<h3>Cooking</h3>\r\n\r\n<p>In a hot pan, pour in three generous tablespoons of olive oil, pour in the egg mixture.</p>\r\n\r\n<p> </p>\r\n\r\n<p>Cook gently over very low heat, making sure the omelette doesn't stick.</p>\r\n\r\n<p>Then flip onto a plate, oil the pan a bit more, and cook the other side.</p>\r\n\r\n<p>Serve hot or warm, with a mesclun salad.</p>\r\n\r\n<p>Also great to take on a picnic, out on the grass at the May Day feast.</p>\r\n\r\n<p> </p>\r\n",
"summary": {
"value": "<p>The famous Niçoise omelette recipe</p>\r\n",
"format": "basic_html",
"processed": "<p>The famous Niçoise omelette recipe</p>"
}
},
"relationships": {...},
"links": {
"self": {
"href": "http://ndd.tld/api/allRecipes/b585c399-284f-443f-8440-64df2225ada6?resourceVersion=id%3A68"
}
}
},

Gatsby
Now it’s time to open your terminal and install Gatsby!
npm install -g gatsby-cli
A few seconds later, Gatsby is installed and we can start creating our first site
gatsby new pwa3
Go into this directory and launch the command gatsby develop to start the development server.
You can see your first Gatsby site at: http://localhost:8000/

Meanwhile, start up your favorite code editor for a little tour, and let’s first look at the src/pages directory, which contains some static pages.
The homepage is represented by the index.js file.
Here it is with a few small changes:
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>Hello everyone</h1>
<p>Welcome to my first Gatsby site</p>
<p>It's missing a few recipes, don't you think?</p>
<div style={{ maxWidth: `300px`, marginBottom: `1.45rem` }}>
<Image />
</div>
<Link to="/page-2/">Go to page 2</Link>
</Layout>
)
export default IndexPage
Notice that the important HTML code of this page is included inside a <Layout> tag, which is a React component.
And as it happens, it’s in 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
You’ll see <main>{children}</main>, which is the placeholder for the code of our homepage.
There’s also another component <Header siteTitle={data.site.siteMetadata.title} /> that takes a strange property (props in React terminology): {data.site.SiteMetadata.title}. Where does it come from?
The answer is a little higher up the page: there’s a <StaticQuery> component that executes a GraphQL query to retrieve those specific data. So, the Header's title is extracted from a query.
The source of this title is found in 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: `Ourselves`,
},
You can make a few changes and, when you save the file, note how the page refreshes automatically—thanks, Gatsby!
All this might seem a bit obscure, but fortunately Gatsby also provides an interface for testing our GraphQL queries at:
http://localhost:8000/_graphql
In the left column, copy and paste this query (which matches our StaticQuery) and hit play.
query SiteTitleQuery {
site {
siteMetadata {
title
}
}
}
There’s our SiteMetada.title:
{
"data": {
"site": {
"siteMetadata": {
"title": "A recipe site"
}
}
}
}
You’ll notice that the gatsby-config.js file has other values too, like a description and an author. Want to fetch them? Here’s the query:
query SiteTitleQuery {
site {
siteMetadata {
title,
author,
description
}
}
}
And you’ll be happy to see that the interface offers you autocompletion for existing data.
{
"data": {
"site": {
"siteMetadata": {
"title": "A recipe site",
"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."
}
}
}
}
Let’s go back to our src/Layout.js file and update the query with our new data.
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>
</>
)}
/>
)
Isn’t that great?

That's cool, but where’s Drupal?
Gatsby uses GraphQL to fetch the data to display, so we need a module to query Contenta’s JSON API and get our recipes as GraphQL.
Luckily, there’s already a plugin for that.
Let's stop our development server and install this famous module!
npm install --save gatsby-source-drupal
After (yet again...) a moment, the module is installed, but we still need to add it to gatsby-config.js, in the plugins section.
{
resolve: `gatsby-source-drupal`,
options: {
baseUrl: `http://ndd.tld/`,
apiBase: `api`,
},
},
Let’s start the dev server again with gatsby develop and play with the GraphQL interface.
You'll notice that autocompletion now brings up new sources!
{
allRecipes {
edges {
node {
title
}
}
}
}
allRecipes gives us access to all the results; the previous query displays the title of every node of that content type.
{
"data": {
"allRecipes": {
"edges": [
{
"node": {
"title": "La Trouchia (omelette aux blettes)"
}
},
{
"node": {
"title": "Fiery chili sauce"
}
},
{
"node": {
"title": "Crema catalana"
}
}
]
}
}
}
Victory! But how do you use this in Gatsby?
Let’s use the src/pages/page-2.js page for our experiments and, first of all, import GraphQL at the top of the page:
import { graphql } from "gatsby"
Then create a query variable with our Drupal query and a few more fields (number of servings and 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>A recipe for { data.allRecipes.edges[0].node.numberOfservings} person(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
It's basic and not at all dynamic in the sense that I’ve specified the item in the list of recipes I wanted (edges[0]), but it works.

We use a Gatsby-specific directive to handle the raw HTML returned by the query:
<div dangerouslySetInnerHTML={{ __html: data.allRecipes.edges[0].node.instructions }}></div>
Build the site
gatsby build
and the generated static site is found in the public directory.
In an upcoming article, we’ll see how to dynamically generate a page for each recipe node and create a nice list of the latest recipes.
Ludovic Coullet