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.

<?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:

<?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:

npx create-next-app@latest

Follow the steps to install, as such below (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:

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:

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 though, 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 gql/home.gql.ts and paste in the following.

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 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.

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:

# 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:

/** @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 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!

Comments

Comment on this blog post by publicly replying to this Mastodon post using a Mastodon or other ActivityPub/​Fediverse account.