Icon
Get In Touch
#nextjs

Integrating Contentful (TypeScript)

This documentation provides a step-by-step guide on integrating Contentful with a Next.js 14 project using TypeScript and the App Router.

Install Dependencies

1
npm install contentful @contentful/rich-text-react-renderer

Create Contentful Client

Create a contentful.ts file in the src/lib directory to configure the Contentful client:

1
// src/lib/contentful.ts
2
import { createClient } from "contentful";
3
4
const client = createClient({
5
space: process.env.CONTENTFUL_SPACE_ID || "",
6
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN || "",
7
});
8
9
export const getEntries = async () => {
10
const entries = await client.getEntries();
11
return entries.items;
12
};
13
14
export const getEntryBySlug = async (slug: string) => {
15
const entries = await client.getEntries({
16
content_type: "posts",
17
"fields.slug": slug,
18
});
19
return entries.items[0];
20
};

Fetch Contentful Entries in a Blog Page

Create a blog.tsx file to fetch and display blog entries:

1
import React from "react";
2
import Link from "next/link";
3
import { getEntries } from "@/lib/contentful";
4
5
type Post = {
6
sys: { id: string };
7
fields: {
8
title: string;
9
slug: string;
10
cover?: {
11
fields: {
12
file: {
13
url: string;
14
};
15
};
16
};
17
date: string;
18
};
19
};
20
21
async function fetchPosts(): Promise<Post[]> {
22
const entries = await getEntries();
23
return entries
24
.map((entry: any) => ({
25
sys: entry.sys,
26
fields: entry.fields,
27
}))
28
.filter(
29
(entry: Post) =>
30
entry.fields &&
31
entry.fields.slug &&
32
entry.fields.title &&
33
entry.fields.date
34
);
35
}
36
37
const Blog = async () => {
38
const posts = await fetchPosts();
39
40
return (
41
<div>
42
<h1>Blog</h1>
43
<div>
44
{posts.map((post) => (
45
<Link href={`/blog/${post.fields.slug}`} key={post.sys.id}>
46
key={post.sys.id}
47
>
48
<div
49
style={{
50
backgroundImage: post.fields.cover
51
? `url(${post.fields.cover.fields.file.url})`
52
: "none",
53
backgroundSize: "cover",
54
}}
55
></div>
56
<div>
57
{post.fields.title}
58
<p>
59
{post.fields.date}
60
</p>
61
</div>
62
</Link>
63
))}
64
</div>
65
</div>
66
);
67
};
68
69
export const revalidate = 10;
70
71
export default Blog;

Create a Dynamic Route for Blog Posts

Create a [slug].tsx file in the src/app/blog directory to handle dynamic routing for individual blog posts:

1
import { getEntryBySlug } from "@/lib/contentful";
2
import { documentToReactComponents } from "@contentful/rich-text-react-renderer";
3
import Links from "@/components/Nav/Links";
4
import Link from "next/link";
5
import React from "react";
6
import { IoArrowBackCircleOutline } from "react-icons/io5";
7
import { Metadata, ResolvingMetadata } from "next";
8
9
// Define the structure of the Contentful entry
10
type PostFields = {
11
title: string;
12
body: any;
13
cover: any;
14
15
type Post = {
16
fields: PostFields;
17
};
18
19
// Define the props for the component
20
type PostProps = {
21
post: Post;
22
};
23
24
export async function generateMetadata(
25
{ params }: { params: { slug: string } },
26
parent: ResolvingMetadata
27
): Promise<Metadata> {
28
const entry = await getEntryBySlug(params.slug);
29
if (!entry || !entry.fields) {
30
return {
31
title: "Post not found",
32
};
33
}
34
35
const previousImages = (await parent).openGraph?.images || [];
36
37
return {
38
title: (entry.fields.title ?? "Default Title") as string,
39
};
40
}
41
42
// Server component to fetch the data
43
const BlogPost: React.FC<{ params: { slug: string } }> = async ({ params }) => {
44
let post: Post | null = null;
45
46
try {
47
// Fetch the data
48
const entry = await getEntryBySlug(params.slug);
49
50
// Check if the data is valid and has the expected structure
51
if (entry && entry.fields) {
52
// If it does, assign it to the post variable
53
post = { fields: entry.fields as PostFields };
54
}
55
} catch (error) {
56
console.error("Error fetching blog post:", error);
57
// Handle error appropriately
58
// For now, let's log the error and continue rendering with post set to null
59
}
60
61
// Check if post is null before rendering
62
if (!post) {
63
// Render loading state or error message
64
return <div>Loading...</div>;
65
}
66
67
return (
68
<div
69
<h1>{post.fields.title}</h1>
70
<div
71
className=" h-52 w-full bg-slate-500 bg-center"
72
style={{
73
backgroundImage: `url(${post.fields.cover.fields.file.url})`,
74
backgroundSize: "cover",
75
}}
76
></div>
77
{documentToReactComponents(post.fields.body)}
78
</div>
79
);
80
};
81
82
export default BlogPost;

Configure Environment Variables

Ensure you have the necessary environment variables in your .env.local file:

1
CONTENTFUL_SPACE_ID=your_space_id
2
CONTENTFUL_ACCESS_TOKEN=your_access_token

©2024 Codeblockz

Privacy Policy