CraftCMS, Gastby & Netlify with Live Preview! (Part 4) Cover

CraftCMS, Gastby & Netlify with Live Preview! (Part 4)

Welcome to this multi-part series on building a blog using CraftCMS and Gatsby. We will cover CraftCMS & GraphQL configuration, GatsbyJs setup & configuration, discuss and implement my approach to CraftCMS Live Preview with Gatsby, and finally we will cover deploying on Netlify.

This article assumes that you are familiar with the basics of developing CraftCMS locally using Nitro (v2), deploying to production and all it's parts (hosting, domain configuration, etc), building websites using GatsbyJS and deployed them to Netlify.

Table of Contents

Part 1: Overview

Part 2: Headless CraftCMS

Part 3: Gatsby + Craft (with SEOmatic)

Part 4: Live Preview

Part 4: Implementing Live Preview.

In this part we will install and configure Gatsby to query Craft content.

Acknowledgements

I know it's been a while since Part 3, but life happens and I happen to live on it 😎. Things got hectic when baby Bella got a bit of a cold, but all is well now and we can get back to it.

Setup

Until now, the gatsby end of things has been un-styled, so I just added some Bootstrap to it. I will put the link to final Gastby repo in the end of the article.

Abstracting Craft's Entry Section And Type

To be able to quickly implement Craft's Live Preview in a way that isn't repetitive, we have to create an interface that can render the entry, regardless if it's at build time or at the client.

We will also make use of both typeHandle and sectionHandle to identify and render the appropriate component.

We will first refactor for the build render so we can later use on the client.

First, let's add typeHandle and sectionHandle to our PostsQuery on gatsby-node.js:

// ...
const result = await graphql(`
  query PostsQuery {
    posts: allBlogDefaultEntry {
      nodes {
        uid
        uri
        title
        typeHandle
        sectionHandle
    // ...
  }
`)
// ...

Now that we have access to the section and it's entry type, we can map components to those types just like we did for ContentBlocks.

Inside the components directory, we will add components in the following structure:

./src/components/Craft/
├── Sections/
├──── index.js
├──── Blog/
├────── index.js
├────── EntryTypes/
├──────── defaultType,js

I know, I know... Do we really need all this nesting for three components? In my experience, organizing the components this way pays off when you start to add more sections and entry types to Craft. In some client project I've even gone a step further and created a base entry type component wrapper that I use for different entry types!

Next we can move the ./src/templates/post.js code to ./src/components/Craft/Sections/Blog/EntryTypes/defaultType.js:

import React from 'react'
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
import { Helmet } from 'react-helmet'
import parse from 'html-react-parser'
import { ContentBlocks } from '../../../ContentBlocks'
import { Layout } from '../../../../Layout'

export const DefaultType = entry => {
  const [coverPicture] = entry.coverPicture
  return (
    <Layout>
      { entry.seomatic && (
        <Helmet>
          {parse(entry.seomatic.metaTitleContainer)}
          {parse(entry.seomatic.metaJsonLdContainer)}
          {parse(entry.seomatic.metaLinkContainer)}
          {parse(entry.seomatic.metaTagContainer)}
        </Helmet>
      )}
      <h1 className="mb-2">{entry.title}</h1>
      <GatsbyImage
        alt={coverPicture.title}
        image={getImage(coverPicture.detailCover)}
        className="mx-auto d-block mb-5"
      />
      <ContentBlocks blocks={entry.contentBlocks} />
    </Layout>
  )
}

Did you notice that I've renamed the prop post to entry? As we make the components more modular, it's helpful to borrow from Craft's naming convention.

Now let's add code to ./src/components/Craft/Sections/Blog/index.js:

import React from 'react'
import { DefaultType } from './EntryTypes/defaultType'

export const BlogSection = entry => {

  const entryType = {
    'default': DefaultType,
    'notFound': null
  }

  const EntryType = entryType[entry.typeHandle || 'notFound']

  return EntryType !== null &&  (
    <EntryType {...entry} />
  )
}

And to ./src/components/Craft/Sections/index.js:

import React from 'react'
import { BlogSection } from './Blog'

export const Sections = ({ entry }) => {

  const sectionType = {
    'blog': BlogSection,
    'notFound': null
  }

  const Section = sectionType[entry.sectionHandle || 'notFound']

  return Section !== null &&  (
    <Section {...entry} />
  )
}

Much like ContentBlocks, these components maps the different entry types to the proper component, only rendering mapped components.

With these components in place, we can refactor gatsby-node.js:

// ...
const result = await graphql(`
  query PostsQuery {
    entries: allBlogDefaultEntry { 
    // ...
  }
`)

// ...

const { entries } = result.data

entries.nodes.forEach(entry => createPage({
  path: entry.uri,
  component: path.resolve('./src/templates/base.js'),
  context: { entry }
}))

First we renamed posts to entries, keeping with Craft's naming convention. Next let's rename ./src/templates/post.js to ./src/templates/base.js and modify the code:

import React from 'react'
import { Sections } from '../components/Craft/Sections'

const Base = ({ pageContext }) => {
  const { entry } = pageContext

  return <Sections entry={entry} />
}

export default Base

Now entry pages will render down from Base all the way down to it's EntryType.

Besides all the changes we made, everything should be looking the same. Let's start with Live Preview!

Live Preview (Finally 😮‍💨)

First let's create the page that will render the preview. In Part 2 we set the Blog section Preview Target to live-preview/?uid={sourceUid}&section={section.handle}.

Create ./src/pages/live-preview.js:

// ./src/pages/live-preview.js
import React from 'react'

const LivePreview = () => {
  return <h1>Live Preview Coming Soon!</h1>
}

export default LivePreview

If you head back to Craft's Hello World blog entry and try to live preview it, you should see a big Live Preview Coming Soon!.

Because we will be querying the preview on the fly, we will use react-query and graphql-request to fetch data from Craft's GraphQL api. We will also need query-string to easily get the query parameters provided by the Live Preview request. Let's add them:

$ yarn add react-query graphql-request query-string

Now let's update ./src/pages/live-preview.js to make use of react-query:

// ./src/pages/live-preview.js
import React from 'react'
import { QueryClient, QueryClientProvider } from 'react-query'
import { LivePreviewClient } from '../components/Craft/LivePreviewClient'

const queryClient = new QueryClient()

const LivePreview = ({location}) => {
  return (
    <QueryClientProvider client={queryClient}>
      <LivePreviewClient location={location}  />
    </QueryClientProvider>
  )
}

export default LivePreview

Couple of things happening here. First we setup the QueryClient and it's provider. Then we are grabbing the location prop to be passed down to the LivePreviewClient component:

// ./src/components/Craft/LivePreviewClient/index.js
import React from 'react'
import { parse } from 'query-string'
import { CraftCMSQuery } from './api'
import { Sections } from '../Sections'

export const LivePreviewClient = ({location}) => {

  const searchParams = parse(location.search)

  const { isLoading, data } = CraftCMSQuery(searchParams)

  if (isLoading) return (
    <p>Loading...</p>
  )

  console.log(data);

  const { entry } = data

  return entry && <Sections entry={entry} />
}

Here is where things get fun. We extract the search params from location and pass it to CraftCMSQuery:

// ./src/components/Craft/LivePreviewClient/api.js
import { useQuery } from 'react-query'
import { PostRequest } from './requests/postRequest';

const apiEndpoint = `http://headless-craft.test/api`

export const CraftCMSQuery = (searchParams) => {

  const {uid, section} = searchParams

  const queryString = Object.keys(searchParams).map(function(key) {
    return key + '=' + searchParams[key]
  }).join('&');

  const request = {
    apiUrl: `${apiEndpoint}?${queryString}`,
    uid
  }

  const sections = {
    'blog': PostRequest(request),
  }

  return useQuery(
    ['craft', queryString],
    sections[section],
    {
      // refetchIntervalInBackground: 1000,
      staleTime: 1000,
    }
  )
}

Again, we are mapping the section type to the mapped request component, so we can implement more requests as needed. We then pass the request component to the useQuery hook, with the queryString as the unique key used internally for refetching, caching, and sharing of queries.

Let's add ./src/components/Craft/LivePreviewClient/requests/postRequest.js:

import { gql, request } from "graphql-request";

const PostPreviewQuery = gql`
  query PostQuery($uid: [String]) {
    entry(uid: $uid) {
      ... on blog_default_Entry {
        uid
        uri
        title
        typeHandle
        sectionHandle
        seomatic {
          metaTitleContainer
          metaTagContainer
          metaLinkContainer
          metaJsonLdContainer
        }
        coverPicture {
          ... on mainFileUploads_Asset {
            url: url @transform(width: 960, height: 480, quality: 80, mode: "crop")
            title
          }
        }
        contentBlocks {
          ... on contentBlocks_richText_BlockType {
            uid
            typeHandle
            body
          }
          ... on contentBlocks_picture_BlockType {
            uid
            typeHandle
            image {
              ... on mainFileUploads_Asset {
                url
                title
              }
            }
          }
        }
      }
    }
  }
`
export const PostRequest = ({ apiUrl, uid }) => {
  return () => request(
    apiUrl,
    PostPreviewQuery,
    {uid}
  )
}

Couple of things happening here: PostRequest returns a callback function with the PostPreviewQuery GraphQL query, which is a modified version of the query we are using in gatsby-node.js.

This is where thing get 💩: there is no easy way to reuse graphql strings dues to various reasons, and due to that, it's a place that can grow pretty quick the more entry types you have. For now it's just part of life.

One thing to note is that when it comes to images, we can't use childImageSharp or gatsbyImageData. Instead we will rely on Craft to create the transforms at the sizes we are using. I found that it's ok for generating previews.

And with that, if we try the live preview in Craft again, we should see most of the page content, except for the images. Let's fix that!

Fixing the preview images

The reason the images on live preview won't show is because we've been using the GatsbyImage component. Let's create an Img that will return the appropriate image component based on the available props:

// ./src/components/Img.js
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
import React from 'react'

export const Img = props => {

  const { image, alt, ...rest } = props

  if ('localFile' in image) {
    return (
      <GatsbyImage
        alt={alt}
        image={getImage(image.localFile)}
        {...rest}
      />
    )
  }

  return (
    <img
      alt={alt}
      src={image.url}
      {...rest}
    />
  )
}

We can then update the post cover picture:

// ...
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
import { Img } from '../../../../Img'
// ...
export const DefaultType = entry => {
  const [coverPicture] = entry.coverPicture
  return (
    <Layout>

      <GatsbyImage
        alt={coverPicture.title}
        image={getImage(coverPicture.localFile)}
        className="mx-auto d-block w-100 mb-5"
      />
      <Img
        alt={coverPicture.title}
        image={coverPicture}
        className="mx-auto d-block w-100 mb-5"
      />

    </Layout>
  )
}

and the picture block:

import React from 'react'
import { GatsbyImage, getImage } from 'gatsby-plugin-image'
import { Img } from '../../Img'

export const PictureBlock = ({ image }) => {
  const [ pic ] = image
  return <GatsbyImage
    alt={coverPicture.title}
    image={getImage(coverPicture.localFile)}
    className="mx-auto d-block w-100 mb-5"
  />
  return <Img
    alt={pic.title}
    image={pic}
    className="mx-auto d-block w-100 mb-5"
  />
}

And now we can live preview blog posts and it's images. You can find the complete gatsby code for this tutorial here.

Stay tune for Part 5, where we will over deploying it to Netlify.