Live Previews with WordPress and Gatsby

Live Previews with WordPress and Gatsby

Want to hop right to the code? GitHub Repository is here.

There are so many headless WordPress GitHub examples now, whether they be tutorials, GitHub repos or actual production sites. Astounding considering where the space was just a few years ago. This is, yet another example, although perhaps more comprehensive in what it includes.

Previews! Where are my previews?

Batteries included 🔋. This repository includes the following:

  • A Gatsby Theme that provides a Higher Order Component for live Previews.
  • A WordPress Docker Container.
  • A “Headless” WordPress Theme.
  • WPGraphQL plugin
  • A Gatsby Starter that demonstrates how to use the theme’s higher order component to facilitate previews for WordPress posts and custom post types.

Quick start

  • Clone the repo
  • Rename theme/sample.env > .env & enter creds. Hint: current creds will work.
  • In the root of the repo run yarn
  • docker-compose up
  • Run through WordPress install @ http://localhost:3030
  • Activate WPGraphQL plugin
  • Activate WP Headless theme
  • Enable Permalinks (any will do)
  • Fire up the Gatsby Starter Site: In the root of the project run yarn workspace site develop
  • Navigate to the “Hello World” post, change something & click the preview button

Using the preview higher order component

You can see an example implementation in the demo site here.

If you’re familiar with Gatsby, this component should look pretty familiar. If you’re familiar with WPGraphQL line 41 should look familiar as well.

const PREVIEW_QUERY = gql`
  query getPreview($id: Int!) {
    postBy(postId: $id) {
      title
      revisions {
        nodes {
          id
          title
          content
        }
      }
    }
  }
`;

To support previews, a GraphQL preview query must be written and passed to the withPreview higher order component as the HOC makes no assumptions on the shape of your query.

export default withPreview({ preview: PREVIEW_QUERY })(PostTemplate);

Gatsby preview URLs

Are the same URLs Gatsby Builds + three parameters; preview=true, post=theWordPressPostID` and nonce=wordPressGeneratedNonce.

An example:

// Live URL built by Gatsby.
https://justinwhall.com/blog/hello-world/

// Preview URL.
https://justinwhall.com/blog/hello-world/?preview=true&post=1&nonce=ajd5end6

Writing the preview query

You may have noticed we passed a preview arg to the withPreview higher order component above. For any given post, page or customer post type, your query and subsequently preview query could be different as such the withPreview makes no assumptions on the shape your query so you must provide one and pass as an argument.

You can see an example of this in the post template in the demo site.

const PREVIEW_QUERY = gql`
  query getPreview($id: Int!) {
    postBy(postId: $id) {
      title
      revisions {
        nodes {
          id
          title
          content
        }
      }
    }
  }
`;

This is subsequently passed as argument to the withPreview higher order component.

export default withPreview({ preview: PREVIEW_QUERY })(PostTemplate);

Fetching live data with Apollo & WPGraphQL

There is some set up here. We need to wrap the ApolloProvider around our app. You can see that in gatsby-browser.js and gatsby-ssr.js here and here respectively.

We also need to initialize the ApolloClient so we can import and use Apollo’s <Query> component in the withPreview component.

import { ApolloClient } from 'apollo-boost';
import { ApolloLink } from 'apollo-link';
import { setContext } from "apollo-link-context";
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-fetch';

const cache = new InMemoryCache();

export const client = new ApolloClient({
    cache,
    fetch,
    link: ApolloLink.from([
        setContext((req, prev) => {
          return ({
            headers: {
              ...prev.headers,
              // As long as we are on the same domain with or WP install and Gatsby front end, we can use the x-wp-nonce header to authenticate and fetch previews.
              "X-WP-Nonce": req.variables.nonce ? req.variables.nonce : '',
            },
          })
        }
        ),
        new HttpLink({
            uri: 'http://localhost:3030/graphql',
            credentials: 'include',
        }),
    ])
});

The withPreview higher order component is simple in principle. The logic here is as follows:

  • Determine if the request is a preview.
  • If it isn’t, return the component and we’re done.
  • If it is…
  • Pass the post (WordPress Post ID) and nonce to Apollo & execute a client side query to the WPGraphQL endpoint.
  • Return the preview data back to the component.
  • Render the preview data in the component.

So let’s do it.

Determine if the request is a preview and If it isn’t, return the component and we’re done.

const withPreview = (args = { preview: false }) => Component => {
  const preview = (props) => {
    
    /**
     * Parse the URL. Using the example above ( https://justinwhall.com/blog/hello-world/?preview=true&post=1&nonce=ajd5end6 )
     */
    const parsed = queryString.parse(props.location.search);
    const {
      nonce, // ajd5end6
      preview, // true
      post, // 1
    } = parsed;

    // Id needs to be an int for preview query.
    const id = parseInt(post, 10);

    /**
     * If no preview param, return the component with the preview props as false.
     */
    if (!preview) {
      return (
        <Component
          preview={false}
          {...props}
        />
      );
    }
    
  }
};

Pass the post (WordPress Post ID) and nonce to Apollo & execute a client side query to the WPGraphQL endpoint and return the preview data back to the component.

import React from 'react';
import { Query } from 'react-apollo';
import queryString from 'query-string';

const withPreview = (args = { preview: false }) => Component => {
  const preview = (props) => {

    const parsed = queryString.parse(props.location.search);
    const {
      nonce,
      preview,
      post,
    } = parsed;

    // Id needs to be an int for preview query.
    const id = parseInt(post, 10);

    /**
     * If no preview param, return the component with the preview props as false.
     */
    if (!preview) {
      return (
        <Component
          preview={false}
          {...props}
        />
      );
    }

    /**
     * Otherwise, run our Apollo query.
     */
    return (
      <Query
      query={args.preview}
      variables={{
        id,
        nonce,
      }}
      >
          {({ data, loading, error }) => {
            if (loading) return <p>Loading preview...</p>;
            if (error) return <p>Error: ${error.message}</p>;

            return (
              <Component
                preview={data}
                {...props}
              />
            )
          }}
      </Query>
    )
  };

  return preview;
};

export default withPreview;

Lastly, render the preview data in the component.

import React from "react";
import { graphql } from "gatsby";
import gql from 'graphql-tag';
import Layout from "../components/layout";
import withPreview from '../../../theme/src/components/withPreview';

const PostTemplate = (props) => {

  /**
   * Determine if we're looking at a preview or live page.
   */
  const postData = props.preview ?
    props.preview.postBy.revisions.nodes[0] : // grab the first revision 
    props.data.wpgraphql.post

  const {
    title,
    content,
  } = postData;

  return (
    <Layout location={props.location}>
      <h1>{title}</h1>
      <div className="post-content" dangerouslySetInnerHTML={{ __html: content }} />
    </Layout>
  )
}

export const pageQuery = graphql`
  query GET_POST($id: ID!) {
    wpgraphql {
      post(id: $id) {
        title
        content
        uri
      }
    }
  }
`

const PREVIEW_QUERY = gql`
  query getPreview($id: Int!) {
    postBy(postId: $id) {
      title
      revisions {
        nodes {
          id
          title
          content
        }
      }
    }
  }
`;

export default withPreview({ preview: PREVIEW_QUERY })(PostTemplate);

Full repository is located here.