DEV Community

Cover image for Live preview with Craft CMS & Next.js
Myles
Myles

Posted on

Live preview with Craft CMS & Next.js

So you've gone headless but now content authors/editors are nagging you to preview their work before saving it?

Thankfully with Craft and Next.js live previews are possible and don't require to much effort at all.

This tutorial is based on using Craft 3.4+ and Next.js 10+ using GraphQL/Apollo with the frontend hosted on Vercel.com.

Firstly you'll need to configure your Craft sections. You'll need to configure their Preview targets to point to your frontend.

Craft Setup

Example
Alt Text

In this example I have set my preview target to {{alias('@previewBaseUrl')}}?sourceUid={sourceUid}

This Craft alias is set to http://example.com/api/preview

This means you will need to create a new page in Next e.g. /pages/api/preview.js.

The sourceUid that gets passed is the pages unique identifier that Craft will use.

When a user clicks preview in the CMS a unique token is passed which Craft uses to magically display the current draft in combination with the sourceUid.

By default this token is called token but can be changed using the config setting tokenParam

Ok so we've configured Craft, now on to Next....

Next.js Setup

The preview.js file is what we'll use to trigger the Next feature Preivew Mode and Craft's Live Preview.

This file will do the following:

  1. Capture the request
  2. Check for a token and sourceUid
  3. Query for an entry
  4. Trigger Preview mode
  5. Redirect to the above entry with previewData

What Preview Mode basically does is sets a couple of cookies that tells Next that preview mode is active. This is then passed through to the context which we can then use to customise the content the author see's during live preview.

pages/api/preview.js

import { apollo } from '@/config/apollo'
import { ENTRIES_DATA } from '@/gql/entries';

export default async (req, res) => {
    // Check the token and source uid
    if (!req.query.token || !req.query.sourceUid) {
      return res.status(401).json({ message: 'Invalid preview request' })
    }

    // Fetch the headless CMS to check if the provided `uid` exists
    const entry = await apollo(ENTRIES_DATA, { uid: req.query.sourceUid })

    // If the uid doesn't exist prevent preview mode from being enabled
    if (!entry) {
      return res.status(404).json({ message: 'Page not found' })
    }

    // Enable Preview Mode by setting cookies
    res.setPreviewData({
        uid: req.query.sourceUid,
        token: req.query.token
    })

    // Redirect to the path from the fetched entry
    if (entry.data.entry.uri == '__home__') {
        res.redirect("/?uid="+req.query.sourceUid+"&token="+req.query.token)
    } else {
        res.redirect("/" + entry.data.entry.uri + "?uid="+req.query.sourceUid+"&token="+req.query.token)
    }
}
Enter fullscreen mode Exit fullscreen mode

gql/entries.js

import { gql } from '@apollo/client'

export const ENTRIES_DATA = gql`
    query ($uid: [String!]) {
        entry(uid: $uid, limit: 1) {
            title
            uri
            url
            slug
            uid
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

Within the Craft CMS the iframe will first open this preview page, then if an entry is found it will redirect to that using it's uri.

Now to modify your Next.js page to use preview mode to determine what content is shown.

pages/page.js

import { apollo } from '@/config/apollo'
import { ENTRY_DATA } from '@/gql/entry';

function Page(props) {
    return (
        <div>
            <h1>{props.data.entry.title}</h1>
            <p>My amazing websites goes here...</p>
        </div>
    )
}

// note this also works with getServerSideProps
export async function getStaticProps(context) {
    if (context.preview && context.previewData.crafttoken) {
        // Preview mode is active, pass appropiate params e.g. uid and token.
        var { data, errors } = await apollo(ENTRY_DATA, { uid: parseInt(context.previewData.sourceId) }, context.previewData.crafttoken)
    } else {
        // No preview mode, return live content.
        var { data, errors } = await apollo(ENTRY_DATA)
    }
    // return a 404 if no page is found
    if (errors.length > 0) return { notFound: true }
    // return page props e.g. page data and preview data (could be useful to have in the template)
    return { props: { data, preview: context.preview ? context.previewData : [] }  }
}

export default Page;
Enter fullscreen mode Exit fullscreen mode
import { gql } from '@apollo/client'

export const ENTRY_DATA = gql`
    query ($slug: [String!], $uid: [String]) {
        entry(section: "pages", slug: $slug, uid: $uid, limit: 1) {
            id
            title
            slug
            ... on pages_page_Entry {
                customFieldGoesHere
            }
        }
    }
`;
Enter fullscreen mode Exit fullscreen mode

In this example above you can see that in getServerSideProps we check to see if preview mode is active and if so pass the uid and the token through to the GQL query using Apollo.

If it's not then we simple just query the page as normal.

context.preview will be active for as long as those cookies that Next created exist. Clearing these cookies via the Craft CMS is on my todo list 😅. Another option might be to just simply give them a short lifespan e.g. 1-5 mins (you can do this via the setPreviewData as an option).

And there you have it - live preview using Craft and statically generated pages using Next.js 🥳

Bonus

To save you some time I'll post my Apollo setup below which is used to connect to the Craft GQL API and then run the queries.

config/apollo.js

import { ApolloClient, InMemoryCache } from '@apollo/client';

async function apollo(gqlQuery, gqlParams, previewToken = false) {
    // setup the Apollo client
    const client = new ApolloClient({
        // if the craft token exists then append it to the URL used to make queries
        // Craft uses this to load draft/revision data
        uri: previewToken ? process.env.NEXT_PUBLIC_CMS_ENDPOINT+"?crafttoken="+previewToken : process.env.NEXT_PUBLIC_CMS_ENDPOINT,
        cache: new InMemoryCache()
    });

    return await client.query({
        // the GQL query
        query: gqlQuery,
        // pass through query params e.g. { slug: "boop", uid: "2dff-344d-dfdd...", token: "..." }
        variables: gqlParams,
        // display errors from GraphQL (useful for debugging)
        errorPolicy: 'all'
    }).then(data => {
        return { 
            // return the data or NULL if no data is returned
            data: (typeof(data.data) != "undefined") ? data.data : null , 
            // return any errors
            errors: (data.errors) ? data.errors : [] 
        }
    });
}

export { apollo }
Enter fullscreen mode Exit fullscreen mode
Things to note

You may run into a few issues locally if you locally environment isn't using HTTPS on both ends as many browsers these days don't like cookies that aren't secure and come from different URLS e.g. CORS.


Hopefully that gives you a good starting point and saves you some time and stress.

If you have any improvements, let me know in the comments below and I can update the article for reference.

Top comments (0)