Make your Next.js project dynamic

This guide shows you how to connect an existing Next.js project to Prepr to retrieve and show Acme Lease demo content.

You can also watch the video for step-by-step instructions detailed in the guide below.

Connect your Next.js website to Prepr

The steps below continue from the previous section, Set up a Next.js project. If you don't yet have a Next.js website with static pages, follow the steps in this section first. Otherwise, let's get started.

Install the Apollo client

The Apollo client is an integration tool that helps to retrieve CMS data with GraphQL. The instructions below show you how to install the Apollo client so that you can add GraphQL queries to request data from the Prepr API.

  1. Stop the localhost website server (CTRL-C) if it's running and execute the following command in the terminal:

    npm install @apollo/client @apollo/experimental-nextjs-app-support
  2. Once done, create a file called apollo-client.ts in the src folder. Copy the following code to this file to import and initialize the Apollo client:

    ./src/apollo-client.ts
    import {HttpLink} from "@apollo/client";
    import { ApolloClient,InMemoryCache, registerApolloClient, } from "@apollo/client-integration-nextjs";
     
    export const {getClient, query, PreloadQuery} = registerApolloClient(() => {
        return new ApolloClient({
            cache: new InMemoryCache(),
            link: new HttpLink({
                // this needs to be an absolute url, as relative urls cannot be used in SSR
                uri: process.env.PREPR_GRAPHQL_URL,
                // you can disable result caching here if you want to
                // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
                // fetchOptions: { cache: "no-store" },
            }),
        });
    });

This client will be used to make API requests to endpoints provided by the Prepr CMS across your Next.js application.

  1. We recommend using environment variables to store sensitive information like access tokens. To add environment variables, create a .env file in the root directory of your project and add the access token like this:

    ./.env
    PREPR_GRAPHQL_URL=<YOUR-PREPR-GRAPHQL-URL>
  2. Replace the placeholder value <YOUR-PREPR-GRAPHQL-URL> with the API URL of an access token from Prepr. Get an access token by logging into your Prepr account:
    a. Go to Settings → Access tokens to view all the access tokens.
    b. Click to open the GraphQL Production access token, and copy the API URL to only retrieve published content items on your site for now. c. Paste the copied API URL in your .env file.

Use the GraphQL Production access token to request published content items for your live app and use the GraphQL Preview token to make a preview of unpublished content items for your content editors.

access token

  1. Execute the following command to restart the server and refresh your page in the browser.

    npm run dev

If your website runs without errors, then the setup above was done correctly. Now that the Apollo client is installed, you can retrieve content from Prepr. Before we create a query to retrieve content, let's follow some best practices for TypeScript and install a GraphQL code generator.

Install a code generator

In this step, you'll install a GraphQL code generator to generate TypeScript types automatically for your queries and keep these up to date with any schema changes. Check out the TypeScript best practices doc for more details. In the meantime, follow the steps below to install the GraphQL code generator.

  1. Stop the localhost website server (CTRL-C) if it's running and execute the following commands in your terminal:

    npm install graphql
    npm install -D typescript @graphql-codegen/cli
  2. Once done, initialize the setup wizard with the following command:

    npx graphql-code-generator init
  3. Choose the Application built with React option.

codegen wizard

  1. For the Where is your schema? question, use the same GraphQL URL you pasted in the .env for the PREPR_GRAPHQL_URL variable.

  2. For the remaining questions, choose the options as shown in the image below.

codegen wizard

  1. Install additional plugins with the following command.

    npm install @graphql-codegen/client-preset @graphql-codegen/typescript @graphql-codegen/typescript-react-apollo @graphql-codegen/typescript-resolvers
  2. If successful, you'll see a new file, codegen.ts in the root of your project. Update it with the highlighted code below.

    ./codegen.ts
    import type {CodegenConfig} from '@graphql-codegen/cli';
     
    const config: CodegenConfig = {
        overwrite: true,
        schema: "<YOUR-PREPR-GRAPHQL-URL>",
        documents: ['!src/gql/**/*', 'src/queries/**/*.{ts,tsx}'],
        generates: {
            "src/gql/": {
                preset: 'client',
                plugins: [],
                presetConfig: {
                fragmentMasking: { unmaskFunctionName: 'getFragmentData' },
                },
                config: {
                    reactApolloVersion: 3,
                },
            },
            "./graphql.schema.json": {
                plugins: ["introspection"]
            }
        }
    };
     
    export default config;
  3. Execute the following command to restart the server and refresh your page in the browser to make sure the Apollo client is installed correctly:

    npm run dev

If your website runs without errors, then the setup above was done correctly. Now you can add a query to your project.

Add a GraphQL query

Once the GraphQL code generator is installed, you can add a query to get the content for your home page from Prepr.

If you’re using preloaded demo data in your Prepr environment as mentioned in the Prerequisites, you should have a Homepage content item like in the images below. The Homepage has all the elements of the page in a Stack field. The stack makes it easy for an editor to set up their page content in a flexible structure for the front end.

Home page content item - stack field

You can see that the hero and feature sections are in adaptive content blocks. We'll explain that in more detail in the upcoming personalization chapter.

Follow the steps below to add a query to retrieve the id, title, slug, and a stack containing the hero and feature sections with their own fields:

  1. Create a queries folder in the src directory of your project and create a file named get-page-by-slug.ts.

  2. Add the following query to this file to retrieve a page by its slug:

You'll notice that we've included fragments in our query. Fragments in GraphQL are a way to define a set of fields that can be reused.

./src/queries/get-page-by-slug.ts
import {graphql} from '@/gql'
 
// The GetPageBySlug query uses the input a slug parameter to retrieve a specific page
export const GET_PAGE_BY_SLUG = graphql(`
    query GetPageBySlug($slug: String) {
        Page(slug: $slug) {
            title
            _id
            content {
                __typename
                ... on Hero {
                    ...Hero
                }
                ... on Feature {
                    ...Feature
                }
            }
        }
    }
`)
 
// A fragment for the feature section
export const FEATURE_SECTION_FRAGMENT = graphql(`
    fragment Feature on Feature {
        _id
        heading
        sub_heading
        button {
            ...Button
        }
        _context {
            variant_key
        }
        image_position
        image {
            url(width: 870, height: 570)
        }
    }
`)
 
// A fragment for the hero section
export const HERO_SECTION_FRAGMENT = graphql(`
    fragment Hero on Hero {
        _id
        sub_heading
        image {
            url(preset: "Hero", width: 2000)
            height
            width
        }
        _context {
            variant_key
        }
        heading
        buttons {
            ...Button
        }
    }
`)
 
// A fragment for the button component
export const BUTTON_FRAGMENT = graphql(`
    fragment Button on Button {
        button_type
        text
        external_url
        link {
            ... on Category {
                _slug
            }
            ... on Page {
                _slug
            }
            ... on Post {
                _slug
            }
        }
    }
`)

You can create and test GraphQL queries using the Apollo explorer (opens in a new tab) from Prepr. Open the API Explorer from the Homepage content item or from your access token page.

Generate TypeScript types

  1. Now that the query is created, generate the TypeScript types by running the following command in the terminal:

    npm run codegen
  2. If the command is successful you'll see a new file called graphql.ts in the gql folder. Check out the TypeScript doc for more details.

  3. Test the query by updating the page.tsx file. The code below executes the query and prints the query results to the console.

    ./src/app/[[...slug]]/page.tsx
    // Import the components to display them 
    import HeroSection from "@/components/sections/hero-section";
    import FeatureSection from "@/components/sections/feature-section";
    import {getClient} from "@/apollo-client";
    import {GetPageBySlugDocument, GetPageBySlugQuery} from "@/gql/graphql";
    import {notFound} from "next/navigation";
     
    export const revalidate = 0
    export const dynamic = 'force-dynamic'
     
    async function getData(slug: string) {
        const {data} = await getClient().query<GetPageBySlugQuery>({
            query: GetPageBySlugDocument,
            variables: {
                slug: slug,
            },
            fetchPolicy: 'no-cache',
        })
     
        if (!data) {
            return notFound()
        }
        return data
    }
     
    // Include parameters to check the slug in the URL
    export default async function Page({params}: { params: { slug: string | string[] } }) {
        let {slug} =  await params
     
        // Set the slug to the home page slug if there's no slug 
        if (!slug) {
            slug = '/'
        }
     
        // Add a forward slash to the slug to navigate to the correct page.
        if (slug instanceof Array) {
            slug = slug.join('/')
        }
     
        const data = await getData(slug)
        console.log(JSON.stringify(data.Page, undefined, 2))
        
        return (
            <div>
                <HeroSection/>
                <FeatureSection/>
                <FeatureSection align='right'/>
                <FeatureSection/>
            </div>
        );
    }

When you check the console in your terminal, the response to the query looks something like this:

query results in console

Now that the query has been created and retrieves the data successfully, fetch and show the specific page content from the query results.

Troubleshooting
Compilation error: In the page.tsx for the line import {GetPageBySlugDocument, GetPageBySlugQuery} from "@/gql/graphql": '"@/gql/graphql"' has no exported member named 'GetPageBySlugDocument'.
Cause: The query name was not defined or is different than expected in the page.tsx.
Solution: Check that the query you created has a query name. In our example the query name is defined as query GetPageBySlug(...){...}. After updating the query, regenerate the TypeScript types.

Fetch page content

To view the Prepr content in your website, you need to fetch the page elements. As mentioned previously, the page has elements in a Stack field. You'll fetch two components, a Hero section and Feature sections in the stack.

  1. The components also include image content so you first need to set up your project to use the Next.js Image component by adding the image domain to the next.config.ts with the following code:

    ./next.config.ts
    import type { NextConfig } from "next";
     
    const nextConfig: NextConfig = {
        /* config options here */
        images: {
            remotePatterns: [
                {
                    hostname: 'demo-patterns.stream.prepr.io',
                },
            ],
        },
    };
     
    export default nextConfig;

    The hostname matches the domain of the demo images included with the demo content you loaded in your environment. When you add your own images and other assets, don't forget to change the hostname in the Next.js configuration to match the domain of your own images.

  2. Next, update your components to display data dynamically.

    ./src/components/sections/hero-section.tsx
    import Link from 'next/link'
    import { FragmentType, getFragmentData } from '@/gql'
    import { HERO_SECTION_FRAGMENT } from '@/queries/get-page-by-slug'
    import Image from 'next/image'
    import Button from '@/components/button'
     
    export default function HeroSection(props: { item: FragmentType<typeof HERO_SECTION_FRAGMENT> }) 
    {
        const item = getFragmentData(HERO_SECTION_FRAGMENT, props.item)
        const image = item.image
        
        return ( 
            <section className="bg-primary-50">
                <div className="mx-auto max-w-8xl p-spacing flex flex-col items-center md:flex-row gap-8 py-10 lg:py-20">
                    <div className="basis-6/12">
                        <h1 className="text-mb-5xl lg:text-7xl text-secondary-700 font-medium break-words text-balance">
                            {item.heading}
                        </h1>
                        <p className="text-mb-lg text-secondary-500 lg:text-lg mt-4 lg:mt-6 text-balance">
                            {item.sub_heading}
                        </p>
                        <div className="flex gap-4 mt-8 xl:mt-10">
                            <div>
                                <Link href='#'><Button>Find your car</Button></Link>
                            </div>
                        </div>
                    </div>
                    <div className="basis-6/12 relative flex justify-end items-center">
                        <div className="z-10 flex items-center aspect-[20/17] w-9/12 overflow-hidden justify-center absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
                            <Image src={image?.url || ''} alt="Hero Image" width={720} height={360} className='object-cover rounded-2xl'/>
                        </div>
                        <div
                            className="w-9/12 aspect-[20/17] bg-primary-100 rounded-3xl right-0 top-0 z-0">
                        </div>
                    </div>
                </div>
            </section>
        )
    }
  3. Now you can fetch and set the data for the page by getting elements from the stack and returning them in the page.tsx file as follows:

    ./src/app/[[...slug]]/page.tsx
    // Import the section components to display them 
    import HeroSection from "@/components/sections/hero-section";
    import FeatureSection from "@/components/sections/feature-section";
    import {getClient} from "@/apollo-client";
    import {GetPageBySlugDocument, GetPageBySlugQuery} from "@/gql/graphql";
    import {notFound} from "next/navigation";
     
    export const revalidate = 0
    export const dynamic = 'force-dynamic'
     
    async function getData(slug: string) {
    const {data} = await getClient().query<GetPageBySlugQuery>({
        query: GetPageBySlugDocument,
        variables: {
            slug: slug,
        }, 
        fetchPolicy: 'no-cache',
    })
     
        if (!data.Page) {
            return notFound()
        }
     
        return data
    }
     
    export default async function Page({ params}: {params: Promise<{ slug: string | string[]}>}) 
    {
        let { slug} = await params
     
        // Set the slug to the home page value if there's no slug 
        if (!slug) {
            slug = '/'
        }
     
        // Add a forward slash to the slug to navigate to the correct page.
        if (slug instanceof Array) {
            slug = slug.join('/')
        }
     
        const data = await getData(slug)
        const elements = data.Page?.content.map((element, index) => {
        if (element.__typename === 'Feature') {
            return <FeatureSection key={index} item={element}/>
        } else if (element.__typename === 'Hero') 
            {
                return <HeroSection key={index} item={element}/>
            }
        })
        
        return (
            <div>
                {elements}
            </div>
        );
    }

Now when you view the website on your localhost, you'll see something like the image below:

Dynamic home page

Troubleshooting
Compilation error: Line import {FragmentType, getFragmentData} from "@/gql"; in the components hero-section.tsx and feature-section.tsx -> '"@/gql"' has no exported member named 'FragmentType'.
Cause: The codegen.ts does not have preset definition for query fragments.
Solution: Update the codegen.ts to include the presets for fragments and regenerate the TypeScript types.

Congratulations! You have successfully connected your front end to Prepr to make your website dynamic. Continue your journey to the next section to set up data collection.

Was this article helpful?

We’d love to learn from your feedback