Create dynamic NextJS 13 pages by querying the Keystone GraphQL API

Sep 20, 2023

To create dynamic routes in NextJS app router, refer to this guide: https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes

For this post, we will set up the our page layout to use a slug generated in Keystone to create the dynamic routes.

Assumptions:

  • You have a functioning NextJS site that renders posts from the Kesytone CMS.
  • There is already a post content type/list in Keystone that contains fields for creating posts.
  • You have completed the steps in this post: /post/connecting-nextjs-frontend-to-keystone-graphql and can query the graphQL API.

Add a slug field to Keystone CMS

Add a unique slug field to the Post content type in the schema.js file. The field should have a unique value.

export default postFields = {
  fields: {
    title: text({ isRequired: true }),
    slug: text({ isUnique: true, isIndexed: 'unique' }),
… other fields & settings

PS: If there is already content in the database, you may get it may be necessary to run npm run generate or npm run keystone prisma migrate dev after adding the field. However, this will wipe all data in the database.

We want the slug field to get generated from the title when the post is created, so we will add a hook that checks if the slug exists, and generates one if it doesn't exist. This will only run once when the post is created. If the tiel is edited in the future, the slug will not be changed automatically.

To generate the slug, add the following to the schema.js file:

  fields: {
    ...
  },
  hooks: {
    // Generate a slug based on the title
    resolveInput: async ({ resolvedData, operation, inputData, context }) => {
      if ((operation === 'create') && !inputData.slug) {
        return {
          ...resolvedData,
          slug: resolvedData.title
            .toLowerCase()
            .replace(/ /g, '-')
            .replace(/[^\w-]+/g, ''),
        };
      }
      return resolvedData;
    },

You may also choose to add extra validation to the slug field so manual slug updates follow the slug pattern:

hooks: {
       …
       return resolvedData;
    },
    // Validate manually-entered slug to ensure that it follows the regex pattern above.
    validateInput: async ({ resolvedData, addValidationError }) => {
      if (resolvedData.slug && !isValidSlug(resolvedData.slug)) {
        addValidationError('The slug must consist of lowercase letters and hyphens only and must not start or end with a hyphen.');
      }
    },

Save and restart Keystone. Test adding a new post, the slug should get generated when you create a post.

Create dynamic pages in NextJS

In the NextJS frontend, add the slug field to your graphQL query.

Following the steps in the app router documentation, create a template for your dynamic posts in your desired location. For this post we will create one in ../app/post/[slug]/page.js

Import gql from the apollo client and your client.js file:

import { getClient } from "../../../lib/client"
import { gql } from "@apollo/client";

Create a query to fetch all existing post slugs. This will be used as the params for the dynamic segments. A path will be created for each post using their unique slug value.

// GraphQL query for posts.
const query = gql` query Posts {
  posts {
    slug
  }
}
`

Map through all the fetched slugs and generate params for them.

export async function generateStaticParams() {
  const { data } = await getClient().query({ query });

  return data.posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }))
}

Create a second GraphQL query, this time, querying for a unique post based on its slug. This will fetch all the fields you would like to render for each Post.

const postQuery = gql` query Post($slug: String!) {
  post(where: { slug: $slug }) {
    title
    intro
    publishedDate
    status
    slug
    id
    body {
      document
    }
  }
}`

Fetch this data in your page component and use it to render the post fields on the page:

export default async function Page({params}) {
  const { data } = await getClient().query({
    query: postQuery,
    variables: {
      slug: params.slug
    }
   });

  let{ post } = data;
  let{ title, body, intro, publishedDate, status, id } = post;

  return(
    <main>
      <h1>{title}</h1>
     // .. other fields go here
    </main>
  )
}

You should see each post if you navigate to post/the-page-slug.