Let’s create a completely new website powered with the following Tech Stack:
Next.js 14 - As a modern web framework built by Vercel
App Router - To get out the most of our project and the pages router is outdated
Contentful - Our headless CMS
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
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 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.




Management Token (CMA)


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=xxxxAdd npm packages
npm install @graphql-codegen/cli graphql graphql-request
npm install @graphql-codegen/typescript-graphql-request @graphql-codegen/introspectionCreate 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.graphqlNow 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-cliLet'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-importDone 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-mergeAs 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.tsxAnd 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 devwill 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.

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.



