2024/10/01

Using Craft CMS Headless with Next.js

Craft CMSHeadlessNext.js

Setup Craft CMS

To start, we’ll need to ensure that Craft CMS is set up properly. On an existing craft install, we’ll have to make a few quick changes to ensure our data is readable.

Since we won’t be using the front-end of Craft CMS any more, we can enable headless mode in our config/general.php.

config/general.php
<?php



return GeneralConfig::create()

    // rest of config

    ->headlessMode(true)

;

Next, we need to set up a route so that our API is publicly accessible. In config/routes.php add the following:

config/routes.php
<?php



return [

    'api' => 'graphql/api',

];

That’s it for Craft CMS, onto Next.js!

Setup Next.js

Let’s take this from the top and treat it like we’re setting up a new app.

Install the latest Next.js, in your terminal run the following:

pnpx create-next-app@latest

Follow the steps below to install (or to your personal preference).

Image showing the steps taken in the terminal to set up Next.js 14

Adding some Variables

Before we can request any data, we’ll want to set up some environment variables that we will store environment specific information in. Either edit or create a .env file in the root directory and add the following:

.env
CRAFT_CMS_GRAPHQL_TOKEN=YourTokenGoesHere

CRAFT_CMS_GRAPHQL_ENDPOINT=https://your-site.ddev.site/api

Communicating with Craft CMS

In our Next.js directory, make a new file utils/fetchApi.ts. This will house our logic for fetching our data and will be what we pass our query options to, and add the following:

utils/fetchApi.ts
interface IHeaders {

  "Content-Type": string

  Authorization: string

}



export async functon fetchApi(  

  query: string,

  variables?: Object | {}

) => {

  const headers: IHeaders = {

    "Content-Type": "application/json",

    Authorization: `Bearer ${process.env.CRAFT_CMS_GRAPHQL_TOKEN}`,

  }

  

  const res = await fetch(process.env.CRAFT_CMS_GRAPHQL_ENDPOINT, {

    method: "POST",

    body: JSON.stringify({

      query,

      variables,

    }),

    headers: headers

  })



  if (!res.ok) {

    // This will activate the closest `error.js` Error Boundary

    throw new Error("Failed to fetch data from API")

  }



  const json = await res.json()



  // All returns are in a data object, we break that down here

  return json?.data

}

To explain the above a little, we pass the Bearer token headers here so we can authenticate the API request. If you don’t have any mutators and are just reading data, you can use the default schema and pass a blank token. We still need to pass the Bearer option to the header, as without it the API request will fail. For any mutators, you should set up a proper schema and set the token as appropriate.

Once the data has been fetched, we then process the JSON and return the data object.

Writing some Queries

To tell the pages what data we’ll need, we need to write our GraphQL queries. These will be unique per your site, but I’ve included a basic example below.

Create a new file in at gql/home.gql.ts and add the following.

gql/home.gql.ts
export const HOME_QUERY = `

  query HomeQuery {

    entry(section: "home") {

      title

    }

  }

`

This will return the title from the section in our Craft CMS called “Home”. We export this as a const called HOME_QUERY so we can use it on our home page, and we’ll import this in a moment to combine it with the fetchApi util to grab data.

Pulling it all together

Let’s combine it together.

Open, or create, the file app/page.tsx and populate it with the following content.

app/page.tsx
import { HOME_QUERY } from "@/gql/home.gql";

import { fetchApi } from "@/utils/fetchApi";



export default async function Home() {

  const { title } = await getData();



  return <h1>{title}</h1>;

}



async function getData() {

  const { entry } await fetchApi(HOME_QUERY, {});

  return entry

}

This will bring in our HOME_QUERY which we’re currently only using to get the title. We also bring in the utility we use to wrap this and grab it.

Since we’re using the app directory in Next.js, we then want to utilise the getData() function to return the fetchAPI call and grab the data. As we are getting the data from our Craft CMS entry, we can then destructure this so we can access the single variables easier.

Bonus: Working with Images

If you want to utilise next/image, you’ll run into a couple of issues with remote Images, to mitigate this we need to add it as a remote pattern.

Firstly, let's update our .env with the following variables:

.env
# NextJS Image remote base

IMAGE_DOMAIN_PROTOCOL=https

IMAGE_DOMAIN_HOSTNAME=your-site.ddev.site

IMAGE_DOMAIN_PATH="/uploads/**" 

Update the variables with your domains details, and make sure the IMAGE_DOMAIN_PATH matches the directory you serve your images from on the Craft CMS side.

Next, open up next.config.mjs and add the following:

next.config.mjs
/** @type {import('next').NextConfig} */

const nextConfig = {

    // rest of config

    

  images: {

    remotePatterns: [

      {

        protocol: process.env.IMAGE_DOMAIN_PROTOCOL,

        hostname: process.env.IMAGE_DOMAIN_HOSTNAME,

        port: '',

        pathname: process.env.IMAGE_DOMAIN_PATH,

      },

    ],

  }, 

  

  // rest of config 

}

Once saved, you can re-run npm run dev if it doesn’t update automatically, and you should now be able to return and render the images.

That’s it, you now have a Next.js app talking to Craft CMS.

Hope that helps!