Loading...
nextjs 14 typescript contentful app router - full example
Author Cloudapp
E.G.

Next.js 14 - Complete Example - Typescript / App Router / Contentful with GraphQL - Part 1

March 8, 2024
Table of Contents

Let’s create a completely new website powered with the following Tech Stack:

  1. Next.js 14 - As a modern web framework built by Vercel

  2. App Router - To get out the most of our project and the pages router is outdated

  3. Contentful - Our headless CMS

  4. Typescript - For Javascript with syntax for types

This is the first post out of a series where we will create a website from the ground up.

First, we start the project initialization, then we create the contentful account, and as the last step, we integrate contentful as headless CMS into our Nextjs project.

In the following posts, we will dig deeper and deeper into the project and add functionalities like multi-language support, user registration/authentication, advanced search with Algolia, and a lot more.

All the services used are free and well-suited for smaller projects.

Create a Project with create-next-app

We use the latest version of the create-next-app package

npx create-next-app@latest
create-next-app
create-next-app

I am using VS Code for the development, so please install VS Code https://code.visualstudio.com/download

Open VSCode and if you are using a “MacOS” you can use Command + Shift + P to open the command palette, and now type “shell”. You should see “Shell Command: Install ‘Code’ command in Path” in the dropdown. Please select it and now VSCode is in your path. So you can jump into your previously created folder and type

code .

which will open VScode within your project folder. Now we have to create/register an account at www.contentful.com. After the first login, you should select “Start from scratch”

Contentful start screen
Contentful start screen

Contentful API Token & Space ID

Now we create the needed Api Keys for our .env.local file. First Space_ID, Access_Token, and Preview_Access_Token, and then Management_Token for write/insert operations.

Contentful start right flyout for api keys - part 1
Contentful start right flyout for api keys - part 1
Contentful start right flyout for api keys - part 2
Contentful start right flyout for api keys - part 2
Contentful start right flyout for api keys - part 3
Contentful start right flyout for api keys - part 3
Contentful start right flyout for api keys - part 4
Contentful start right flyout for api keys - part 4

Management Token (CMA)

Contentful right flyout for Content Management Api Key
Contentful right flyout for Content Management Api Key
Contentful pop-up new CMA Api Key
Contentful pop-up new CMA Api Key

Env File and Npm Packages

Create .env.local file for the env variables

##.env.local##
CONTENTFUL_SPACE_ID=xxxx
CONTENTFUL_ACCESS_TOKEN=xxxx
CONTENTFUL_PREVIEW_ACCESS_TOKEN=xxxx
CONTENTFUL_MANAGEMENT_TOKEN=xxxx
CONTENTFUL_PREVIEW_SECRET=xxxx

Add npm packages

npm install @graphql-codegen/cli graphql graphql-request 
npm install @graphql-codegen/typescript-graphql-request @graphql-codegen/introspection

Create file codegen.ts in the project root

##codegen.ts##
// Purpose: Configuration for GraphQL Code Generator
import { CodegenConfig } from "@graphql-codegen/cli";

const endpointOverride = process.env.CONTENTFUL_GRAPHQL_ENDPOINT;
const productionEndpoint = "https://graphql.contentful.com/content/v1/spaces";
export const endpoint = `${endpointOverride || productionEndpoint}/${
  process.env.CONTENTFUL_SPACE_ID
}`;

export const config: CodegenConfig = {
  overwrite: true,
  ignoreNoDocuments: true,
  schema: [
    {
      [endpoint || ""]: {
        headers: {
          Authorization: `Bearer ${process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN}`,
        },
      },
    },
  ],
  generates: {
    "src/lib/__generated/graphql.schema.json": {
      plugins: ["introspection"],
    },
    "src/lib/__generated/graphql.schema.graphql": {
      plugins: ["schema-ast"],
    },
    "src/lib/__generated/sdk.ts": {
      documents: ["src/lib/graphql/**/*.graphql"],
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-graphql-request",
      ],
      config: {
        rawRequest: false,
        inlineFragmentTypes: "combine",
        skipTypename: false,
        exportFragmentSpreadSubTypes: true,
        dedupeFragments: true,
        preResolveTypes: true,
      },
    },
  },
};

export default config;

Adding two lines to package.json in the scripts section

##script section of package.json##
"graphql-codegen:generate": "graphql-codegen -r dotenv/config --config codegen.ts dotenv_config_path=.env.local",
"graphql-codegen:watch": "graphql-codegen --watch -r dotenv/config --config codegen.ts dotenv_config_path=.env.local"
##package.json##
{
  "name": "nextjs13_5-typescript-app-router-contentful",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "graphql-codegen:generate": "graphql-codegen -r dotenv/config --config codegen.ts dotenv_config_path=.env.local",
    "graphql-codegen:watch": "graphql-codegen --watch -r dotenv/config --config codegen.ts dotenv_config_path=.env.local"
  },
  "dependencies": {
    "@graphql-codegen/cli": "^5.0.2",
    "@graphql-codegen/introspection": "^4.0.3",
    "@graphql-codegen/typescript-graphql-request": "^6.2.0",
    "graphql": "^16.8.1",
    "graphql-request": "^6.1.0",
    "next": "14.1.3",
    "react": "^18",
    "react-dom": "^18"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10",
    "eslint": "^8",
    "eslint-config-next": "13.5.6",
    "postcss": "^8",
    "tailwindcss": "^3",
    "typescript": "^5"
  }
}

Creating folders and files for GraphQL

Create folder src/lib and there the file client.ts

##src/lib/client.ts##
import { GraphQLClient } from "graphql-request";

import { getSdk } from "@/lib/__generated/sdk";
import { endpoint } from "../../codegen";

const graphQlClient = new GraphQLClient(endpoint, {
  headers: {
    Authorization: `Bearer ${process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN}`,
  },
});

export const client = getSdk(graphQlClient);

Create folder "graphql" under src/lib/ and within the "graphql" folder create these .graphql files (you can get them from the Github-Repo)

##graphql files##
authorFields.graphql
imageFields.graphql
pageBlogPost.graphql
pageBlogPostCollection.graphql
richImageFields.graphql
seoFields.graphql

Now we create a new folder called contentfulsync in the project root and then we add these lines in the script section of the package.json

npm install contentful-cli

Let's create the import config file for the contenful-cli under contentfulsync/import/config/contentful-import-config.json

##contentfulsync/import/config/contentful-import-config.json##
{
  "contentFile": "./contentfulexport/import/export.json"
}

Create a new folder contentfulsync/export/config

##contentfulsync/export/config/contentful-import-config.json##
{
  "exportDir": "contentfulsync",
  "contentFile": "export.json",
  "downloadAssets": false
}

Import data into Contentful Space

Now we can import the data to our contentful space:

npm run cf-import

Done this the content types and the content are present in our Contentful Space.

What about the Frontend ?

To be able to show the data on the frontend we need to install missing npm packages.

npm install @contentful/rich-text-react-renderer tailwind-merge

As a next step, we add these components under src/components (create the components folder if not exist)

##Components##
ArticleContent.component.tsx
ArticleImage.component.tsx
CtfImage.component.tsx
CtfRichText.component.tsx

And then we can modify our page.tsx under src/app/

import { ArticleContent } from "@/components/contentful/ArticleContent.component";
import { client } from "@/lib/client";
import { notFound } from "next/navigation";

async function BlogPostPage() {
  const [blogPagedata] = await Promise.all([
    client.pageBlogPost({
      slug: "testblogpost",
    }),
  ]);

  const blogPost = blogPagedata.pageBlogPostCollection?.items[0];

  if (!blogPost) {
    // If a blog post can't be found,
    // tell Next.js to render a 404 page.
    return notFound();
  }

  const relatedPosts = blogPost?.relatedBlogPostsCollection?.items;

  if (!blogPost || !relatedPosts) return null;

  return (
    <>
      {/* <Container className="max-w-4xl mt-8"> */}
      <div className="mt-4" />
      <ArticleContent article={blogPost} />
      {/* </Container> */}
    </>
  );
}

export default BlogPostPage;

Deployment

Starting the development server with

npm run dev

will lead to an error because the domain, where the images were served “images.ctfassets.net” is not configured. So let’s go to our next.config.js file and modify it so that it looks like this:

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: { domains: ["images.ctfassets.net"] },
  reactStrictMode: true,
};

module.exports = nextConfig;

Now the development server should start and we can use the URL http://localhost:3000 to see the result.

Preview Page
Preview Page

That’s it. The basics are done and in the next post we will go ahead with the Frontend, then how we can handle additional languages, User registration/login, and a lot more. Stay tuned.

Github Repo

https://github.com/cloudapp-dev/nextjs13_5-typescript-app-router-contentful

If you like what you see, then please support me with a clap or follow me on medium.com.

Related articles