Lauro SilvaLauro Silva

Original notes repo: https://github.com/eggheadio-projects/build-content-rich-progressive-web-apps-with-gatsby-and-contentful-notes

These notes are intended to be used and studied in tandem with Khaled Garbaya’s Build Content Rich Progressive Web Apps with Gatsby and Contentful course.

Right below is the intended outcomes of the course, these are the skills and knowledge you will learn from the course.

Outcomes

1- Data Modeling with Contentful
2- Starting a Gatsby project
3- Using GraphQL to query data in Gatsby
4- Deploying a static site with Netlify
5- Set up automatic redeployment

Prerequisites

Contribute

These are community notes that I hope everyone who studies benefits from. If you notice areas that could be improved please feel free to open a PR!

Table of Contents

1. Model Content in the Contentful Web App

Video

Contentful organizes content into spaces that allows you to group all the related resources for a project together. This includes content entries, media assets, and settings for localizing content into different languages.

  • Each space contains 1 or more content types that define the structure of your content.
  • Each content type can have up to 50 fields that you define this is called content modeling.

2. Model Content programmatically using the contentful-migration tool

Video

Contentful migration tool you could potentially start anywhere with your content model, then refine it as you learn what you truly need.

You need to have the contentful-cli installed and then login. This will open up a browser window and authenticate you to Contentful.

1npm install -g contentful-cli
1contentful login

Export the function. Inside of this function, we’ll have access to the migration object that will allow us to do any sort of manipulations to our content type. First thing we need to do is create the content type, then we need to define its fields

1module.exports = function (migration) {
2 // create the content type
3 const instructor = migration
4 .createContentType("intructor")
5 .name("Instructor")
6 .description("")
7 .displayField("fullName")
8
9 //fields
10 instructor.createField("fullName").name("Full Name").type("Symbol")
11
12 // fields
13 instructor.createField("fullName").name("Full Name").type("Symbol")
14 instructor.createField("slug").name("Slug").type("Symbol")
15 instructor.createField("bio").name("Bio").type("Symbol")
16 instructor.createField("website").name("website").type("Symbol")
17 instructor.createField("twitter").name("Twitter").type("Symbol")
18 instructor.createField("github").name("Github").type("Symbol")
19
20 instructor.createField("avatar").name("Avatar").type("Link").linkType("Asset")
21
22 // appearances
23 instructor.changeEditorInterface("slug", "slugEditor", {})
24 instructor.changeEditorInterface("website", "urlEditor", {})
25 instructor.changeEditorInterface("twitter", "urlEditor", {})
26 instructor.changeEditorInterface("github", "urlEditor", {})
27}

We call the .changeEditorInterface on the content type, and we give it the field ID and then the ID of the widget. We can do the same for the website field and other similar fields.

Now we call the migration on the space that we have (we pass it the file that we want to run, basically our migration code.).

1ontentful space migration --space-id=lkb87t4toc0t instructor.js

The space-id, we can get from Contentful. It’s basically the URL.

We can follow the same steps for other content types.

1// seo.js
2module.exports = function (migration) {
3 const seo = migration
4 .createContentType("seo")
5 .name("SEO")
6 .description("")
7 .displayField("title")
8 seo.createField("title").name("Title").type("Symbol")
9 seo.createField("description").name("Description").type("Symbol")
10 seo.createField("keywords").name("Keywords").type("Symbol")
11}
1// lesson.js
2module.exports = function(migration) {
3 const lesson = migration
4 .createContentType("lesson")
5 .name("Lesson")
6 .description("")
7 .displayField("title")
8 lesson
9 .createField("title")
10 .name("Title")
11 .type("Symbol")
12 lesson
13 .createField("slug")
14 .name("Slug")
15 .type("Symbol")
16 lesson
17 .createField("body")
18 .name("Body")
19 .type("RichText")
20
21 lesson
22 .createField("instructor")
23 .name("Instructor")
24 .type("Link")
25 .validations([
26 {
27 linkContentType: ["instructor"],
28 },
29 ])
30 .linkType("Entry")
31
32 lesson
33 .createField("image")
34 .name("Image")
35 .type("Link")
36 .linkType("Asset")
37
38 lesson
39 .createField("seo")
40 .name("SEO")
41 .type("Link")
42 .validations([
43 {
44 linkContentType: ["seo"],
45 },
46 ])
47 .linkType("Entry")

This code should live next to your website in the repository, so another developer can bootstrap a separate space. For example, for testing.

3. Add Contentful as a data source for Gatsby

Video

Run npx gatsby new and give it the name of the website.

1npx gatsby new jamstacktutorials

Now let’s cd to this directory. Let’s run npm run develop. Now let’s add some Contentful dependency to it. Install the gatsby-source-contentful plugin:

1npm i gatsby-source-contentful

We will need to provide two options — the spaceId and the accessToken. To get this, we need to go to Contentful and Settings, API Keys. We will click on the first entry here, copy the spaceId and copy the Delivery accessToken.

1resolve: `gatsby-source-contentful`,
2 options: {
3 spaceId: `u2hjug1nowzr`,
4 accessToken: `sJJaBCxUdA4BFqtfR_f5y4m9lvmyOHa3siR8iEETKEc`,
5 }

Now go and add content. To do that, we can go to Content, click on Add entry. Once you have all your content created, you can test if this works by going to the GraphQL server that’s provided by Gatsby. And querying for our content.

4. List data entries from Contentful in Gatsby

Video

In this lesson, you will learn how to get a list of entries from Contentful and render it in a Gatsby website.

First, define the GraphQL query that gets all the lessons. Type allContentfulLesson and find the types that you need. For example:

We will paste the query that we already tested into Gatsby:

1export const query = graphql`
2 {
3 allContentfulLesson {
4 edges {
5 node {
6 title
7 slug
8 image {
9 file {
10 url
11 }
12 }
13 }
14 }
15 }
16 }

Let’s import { graphql } from "gatsby". Once the query is done, Gatsby will pass in the data in the props. Here, we can extract data. Inside of data, we will have allContentfulLesson.

Once you have all the data import it, now you can render it. Example:

1import Card from "../components/card"
2;<div className="flex flex-wrap -mx-3">
3 {allContentfulLesson.edges.map(({ node }) => (
4 <Card node={{ ...node, slug: `/lessons/${node.slug}` }} key={node.id} />
5 ))}
6</div>

5. Programmatically create Gatsby pages from Contentful data

Video

We need to go to the gatsby-node.js and write some code here to create the pages. First, we need to export a createPages function.

We can then extract the createPage from the actions object. To be able to create a page, we need a path to a template, which is basically a React component.

We can use GraphQL to query the Gatsby data for all the Contentful lessons.

1// gatsby-node.js
2const path = require(`path`)
3
4exports.createPages = ({ graphql, actions }) => {
5 const { createPage } = actions
6 const lessonTemplate = path.resolve(`src/templates/lesson.js`)
7 const instructorTemplate = path.resolve(`src/templates/instructor.js`)
8 return graphql(`
9 {
10 allContentfulLesson {
11 edges {
12 node {
13 slug
14 }
15 }
16 }
17 }
18 `).then((result) => {
19 if (result.errors) {
20 throw result.errors
21 }
22
23 result.data.allContentfulLesson.edges.forEach((edge) => {
24 createPage({
25 path: `/lessons/${edge.node.slug}`,
26 component: lessonTemplate,
27 context: {
28 slug: edge.node.slug,
29 },
30 })
31 })
32 })
33}

The GraphQL function returns a promise that will contain our result, and inside of the result, we need to check for errors. If so, we throw result.error.

The createPage will accept the path the component, and a context for additional data.

Let’s save this and create our lesson template.

1// lesson.js template
2import React from "react"
3import { graphql } from "gatsby"
4import Layout from "../components/layout"
5import SEO from "../components/seo"
6
7export const query = graphql`
8 query lessonQuery($slug: String!) {
9 contentfulLesson(slug: { eq: $slug }) {
10 title
11 body {
12 json
13 }
14 seo {
15 title
16 description
17 }
18 }
19 }
20`
21
22function Lesson({ data }) {
23 return (
24 <Layout>
25 <SEO
26 title={data.contentfulLesson.seo.title}
27 description={data.contentfulLesson.seo.description}
28 />
29 <div className="lesson__details">
30 <h2 className="text-4xl">{data.contentfulLesson.title}</h2>
31 {documentToReactComponents(data.contentfulLesson.body.json, {
32 renderNode: {
33 [BLOCKS.HEADING_2]: (node, children) => (
34 <h2 className="text-4xl">{children}</h2>
35 ),
36 [BLOCKS.EMBEDDED_ASSET]: (node, children) => (
37 <img src={node.data.target.fields.file["en-US"].url} />
38 ),
39 },
40 })}
41 </div>
42 </Layout>
43 )
44}
45
46export default Lesson

Let’s hit save, and we need to restart our server. Now, if we refresh, you will be able to see the content that was generated.

6. Render Contentful rich text in Gatsby

Video

Let’s take a look first at how this data is sent to us. If we go to the GraphiQL, and then request the body, inside of the body, we can see JSON. In the result here, we have all our nodes, and you can see the type and all the content.

We need a way to parse this JSON to React components:

1npm i @contentful/rich-text-react-renderer @contentful/rich-text-types

In the lesson.js here, we need to add the body to the query. We require the json data from the body.

1export const query = graphql`
2 query lessonQuery($slug: String!) {
3 contentfulLesson(slug: { eq: $slug }) {
4 title
5 body {
6 json
7 }
8 seo {
9 title
10 description
11 }
12 }
13 }

We need to import the documentToReactComponents function from the rich text React renderer.

1import { documentToReactComponents } from "@contentful/rich-text-react-renderer"

We take this function, and in the markup here, we give it the data from the body. It will be data.contentfulLesson.body.json.

1{
2 documentToReactComponents(data.contentfulLesson.body.json, {
3 renderNode: {
4 [BLOCKS.HEADING_2]: (node, children) => (
5 <h2 className="text-4xl">{children}</h2>
6 ),
7 [BLOCKS.EMBEDDED_ASSET]: (node, children) => (
8 <img src={node.data.target.fields.file["en-US"].url} />
9 ),
10 },
11 })
12}

If we save now and run our server again, and once we refresh, we can see indeed here we have the content.

And we add here a second argument, an object configuration. For every h2, we will receive this callback, and then we can return how it will show up.

1[BLOCKS.HEADING_2]: (node, children) => (
2 <h2 className="text-4xl">{children}</h2>
3 ),

To render an image, let’s go to the options here, and then add the BLOCKS.EMBEDDED_ASSET, and return an image with the correct URL. Let’s save this.

1[BLOCKS.EMBEDDED_ASSET]: (node, children) => (
2 <img src={node.data.target.fields.file["en-US"].url} />
3),

You should now see rich text data from Contentful into Gatsby. If you find an error, remove the cache and public folder, and then run again.

7. Use Graphql back-reference to avoid circular dependencies between Content model

Video

We have a lesson that references an instructor but the instructor content type does not have any data about the lessons that are assigned to it. Usually, you would create a reference back in the instructor content type to the lesson but this can cause a circular reference that’s hard to maintain. Fortunately in the Gatsby data layer, GraphQL, we can query the parent node to get its data from its child. That’s called backreference.

If we type lesson, you can see here that we have access to the lesson, even if it’s not linked directly to the instructor in Contentful. Here, we can grab stuff like the title, the image, and so on. Let’s do the same in our code and render a list of lessons.

First thing we need to do is to update this query that we already exported:

1bio
2 website
3 lesson {
4 id
5 title
6 slug
7 image {
8 file {
9 url
10 }
11 }
12 }
13 }
14}

Now that’s available for us, let’s render it. For that, we need to check first if the lesson is not null. Otherwise, we look through that lesson array and we render it. We will use the same Card component that we use in our index page.

1{data.contentfulInstructor.lesson && (
2 <div className="border-t my-6">
3 <h2 className="text-4xl text-grey-dark my-6">Lessons</h2>
4 {data.contentfulInstructor.lesson.map(node => (
5 <Card
6 node={{ ...node, slug: `/lessons/${node.slug}` }}
7 key={node.id}
8 />
9 ))}
10 </div>
11 )}

8. Deploy a Gatsby website on Netlify

Video

Go to the command line. Here, we will paste this. Hit enter:

1git remote add origin git@github.com: Khaledgarbaya/jamstacktutorials.git

Now that we have the remote added, let’s add everything and commit.

Once we have pushed our code to Github, let’s go to Netlify. After you log in, this is your main dashboard. You click new site from Git. Here we click GitHub.

This is our repository. You can see here that it’s a Gatsby project. It will run this Gatsby build command from my server. Let’s deploy the site.

You can see the logs. This is our website being built.

Now, our site is live. We can click on the preview button. You can see here this is our website.

9. Trigger Netlify Builds when content changes in Contentful

Video

Netlify will rebuild our website whenever we push new code, but we also want to trigger the rebuild whenever we do content changes. We can do that using what’s called webhooks. Let’s go to deploy settings, and in build hooks, you can click add build hook. Let’s call this Contentful and hit save.

We grab this URL and go to Contentful. In there, we go into settings, webhooks, and we add webhook. This one, we’ll call it Netlify. We will paste our URL in here, and we select specific events that will trigger this rebuild.

Let’s change something and we hit publish. Now, when we go back to Netlify, and go to deploys, you can see that here, it’s triggered by Contentful.

© Lauro Silva, LLC. All rights reserved.