DEV Community

Matthias Zaunseder
Matthias Zaunseder

Posted on • Updated on

Craft CMS Live Preview with Next.js 9.3 Static Site Generation and GraphQL

Next.js 9.3 was released 9th of March and has a new and exciting feature for all headless CMS users out there: Static Site Generation coupled with Preview Mode

Static Site Generation (SSG for short) allows us to generate a static site with the contents of our headless CMS during the build. The magic function which enables this is getStaticProps. There we can fetch data from any data source and Next.js will use it to generate a static site.

So this is nice, but now you have static HTML/JS/CSS, but your users of the CMS might want to see a preview of their newly created content. Then you want to dynamically render the page again so that the CMS editor can see his changes.

So I tried to combine a headless Craft CMS with a frontend written in Next.js with static site generation and working live preview. The data fetching will happen over the new GraphQL API of Craft.

First of all, you need a working Craft CMS 3.4 installation with activated GraphQL API. See the Installation guide and the GraphQL Getting Started guide for instructions.
Then you also need a running Next.js website. See the Getting Started guide.

Configuration in Next.js

I have implemented everything in TypeScript, so if you use JavaScript, you have to adjust the examples a bit.

This a simple fetch wrapper to be able to make GraphQL Requests to Craft:

utils/fetch.ts

import fetch from 'node-fetch'; // install with `npm install node-fetch`

/**
 * Fetch from Craft GraphQL API
 * @param query GraphQL Query
 * @param previewToken Optional preview token to get draft data
 */
export default async function(
  query: string,
  previewToken?: string
) {
  let craftUrl = 'http://localhost:8888/api';

  if (previewToken && previewToken !== '') {
    craftUrl += '?token=' + previewToken;
  }

  const res = await fetch(craftUrl, {
    method: 'post',
    body: query,
    headers: {
      'Content-Type': 'application/graphql',
    },
  });

  return await res.json();
}

In the page file, where you want to have SSG with a preview, you have to implement the getStaticProps function.

This fetches the data for the Homepage. In my case this is a single section called "Homepage", which has a field named "text".

pages/index.tsx

export const getStaticProps: GetStaticProps = async context => {
  const { data } = await fetch(
    `
    {
      entry(title: "Homepage") {
        ... on homepage_homepage_Entry {
          text
        }
      }
    }
    `,
    context.preview ? context.previewData?.previewToken : undefined
  );

  return {
    props: {
      data
    }
  };
};

The context.previewData.previewToken data is coming from an API route which is shown below. We only supply the previewToken to the fetch function when the context.preview field is true.

pages/api/preview.ts

import { NextApiRequest, NextApiResponse } from "next";
import fetch from "../../utils/fetch";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  // (1)
  // Check for the right query params
  if (!req.query["x-craft-live-preview"] || !req.query.entryUid) {
    return res
      .status(401)
      .json({ message: "Not allowed to access this route" });
  }

  // (2)
  // Get the url from Craft for the specific entry
  const { data } = await fetch(
    `
      {
        entry(uid: "${req.query.entryUid}") {
          url
        }
      }
    `
  );

  if (!data?.entry?.url) {
    return res.status(404).json({
      message: `URL of the entry "${req.query.entryUid}" could not be fetched`
    });
  }

  // (3)
  // Set the token as preview data
  res.setPreviewData({
    previewToken: req.query.token ?? null
  });

  // (4)
  const parsedUrl = new URL(data.entry.url);

  // Redirect to the path from the fetched url
  res.writeHead(307, { Location: parsedUrl.pathname });
  res.end();
};

This function does the following:

  1. The function expects minimum 2 query params in the URL, "x-craft-live-preview" which gets set by Craft automatically in preview mode and "entryUid" so that we have the uid of the currently previewed entry. If they are not present, we will stop the function.

  2. We try to fetch the URL of the entry from Craft so that we can redirect to the right page.

  3. If the query params have a token param, we will set it as preview data.

  4. We use the URL of the Entry and redirect to the path (Next.js doesn't like the domain part of the URL)

Configuration in Craft

You have to configure your section (in my case the Homepage section) with a preview URL which points to the previously built preview API endpoint in Next.js (Next.js frontend is running under localhost:3000): http://localhost:3000/api/preview?entryUid={sourceUid}
It is important to use {sourceUid} and not {uid} because the uid could be different in draft entries, but they are not queryable.

Et voilà: A static generated homepage, which can show a live preview in Craft whenever you edit the content.

Todos

  • localhost:8888 for the Craft CMS and localhost:3000 for Next.js are hardcoded. This should be configured with .env files.

Top comments (5)

Collapse
 
markjoeljimenez profile image
Mark Jimenez

I'm getting context.preview as undefined. Possible reason: github.com/zeit/next.js/discussion...?

Collapse
 
zauni profile image
Matthias Zaunseder

Do you have Craft 3.4 installed?
And can you do a console.log inside the pages/api/preview.ts file, so that we can be sure, that the function gets called correctly?
And what do you get if you log the context variable inside the getStaticProps function?

Collapse
 
markjoeljimenez profile image
Mark Jimenez

Yup! Running Craft 3.4.11. I've also upgraded Next to 9.3.4@canary mentioned in this Github issue: github.com/zeit/next.js/issues/112...

pages/api/preview.ts

// Set the token as preview data
res.setPreviewData({
    previewToken: req.query.token ?? null,
});

console.log(res.getHeaders());

// Output: [Object: null prototype] {
  'set-cookie':
   [ '__prerender_bypass=f65ed8f32b20ac283013dcecb729649e; Path=/; HttpOnly; SameSite=Lax',
     '__next_preview_data=eyJhbGciOiJIUzI1NiJ9.NjhiNjVmNzNmNDdmZWY4MmQwNDkwN2ZjZjY5NDZjYTdlYzFiOWQ5NTIwOWYwNTE2YWU2N2FiZjNkMmI1YWJlM2Q3OTcwOGIxYTk0M2M4MzhmOTkwOWRkNmZiMGZjODY4NDVjMWQ5ZDUzYmU3MTljNjRhZDU2NWQ0Zjg2M2ZiNjJjODJiYWE1MDg1Y2JjZGVlZDVhMTYxZDNmNzkzMjQ1YjY1NzFhMWM3ZmMzY2M4MjMwYjJkYTdhZDUxNGZhYjE4OTg5OGMwODVhOTYwYmU4Y2RkYWExMWQyOGU5ZGJlN2MwYmY5N2Q2NzE0OTc2MWUyMDQyYjFmMTRhODlhYTEzN2RkNzMxMTNiNDk2MDlmNDcxOWI2MjBiNzAzOGE3OTUxODc0NGI3.i9UtAkpXPU5tRhxFVZzTnYXEi27_cRh0SdLdumpc9No; Path=/; HttpOnly; SameSite=Lax' ] }

pages/[page].tsx

export const getStaticProps: GetStaticProps = async (context) => {
    console.log(context);

    // Output: { params: { page: 'page' } }

    ...
}

But when I access the preview page directly (from Craft's iFrame), it sends the correct context:

{ params: { page: 'page' },
  preview: true,
  previewData: { previewToken: '...' } }

Thanks for the quick responses, Matthias!

Thread Thread
 
zauni profile image
Matthias Zaunseder

Does it help to update to the newest Craft 3.4.16? (github.com/craftcms/cms/blob/maste...)
It adds CORS headers with a * so maybe that helps in your case.

Thread Thread
 
zauni profile image
Matthias Zaunseder

@mark
Now I also had the problem that the Next.js frontend didn't update in the live preview. Then I deleted the cookies for the domain where Craft runs and suddenly it worked again... Very strange and at the moment I have no idea what went wrong with the cookies.