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}§ion={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.