0% found this document useful (0 votes)
22 views

How To Build A Blog With The Ghost API and Next - Js

Uploaded by

holircon1218
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
22 views

How To Build A Blog With The Ghost API and Next - Js

Uploaded by

holircon1218
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 68

12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.

js

How to Build a Blog with the Ghost API and Next.js


freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs

Rajdeep Singh April 13, 2023

Ghost CMS is a popular content management system that many devs and companies use to
host their blogs.

It has many features and an editor that's highly optimized for writing. You can even build
different themes using handlebars.js.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 1/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

But if you don't know Handlebars, learning it can be a long and difficult process. If you are
already a Next.js developer and you don't know Handlebars, creating a new theme for your
Ghost-based site can be tough.

In the article, I will teach you how to use Ghost CMS as a backend and Next.js as a frontend.
I will guide you through everything related to Nextjs 13 app directory and the Ghost CMS
API.

Next.js 13 team currently working on the experimental app folder. Next uses file-based
routing with the page directory. The new app directory is based on file system routing and
provides additional functionality like layouts, error handling, component loading, and server-
side and client-side rending out of the box.

All the code is available on GitHub. You can also check out the live demo website.

Table of Contents
In this article, we cover the basics of Next's experimental app directory. Then I'll teach you
how to step up Next and Ghost CMS locally and how to integrate Ghost with Next. Lastly, I'll
show you how to consume data from the backend (via theGhost CMS API ) and show it on
the site with React.js.

Why Use Next.js for the Front End and Not a Ghost CMS Theme?
There are a few reasons why you might consider using Next as the frontend framework for
your blog:

1. Ghost CMS doesn't generate static builds, but Next.js does.


2. You get increased website speed and performance with Next.js and it now provides
built-in SEO support and other optimizations. Ghost doesn't have some of these
features.
3. For React developers, it is easy to build a new blog with Next (since Next is React-
based), and you do not need to learn additional tools.
4. You'll find a few service providers available for Ghost to deploy a Ghost blog with one
click. Most of them come with a paid plan while one or two offer a free plan (but these
tend to have time and feature limitations). For Next.js, many players are available in
the market.

Basically, when it comes to static builds and website performance, Ghost doesn't perform as
well in either case. The alternative is to use a frontend platform like Next, React, Angular, or
Vue.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 2/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

I chose Next because it's a highly in-demand and popular React framework, and plenty of
tools and libraries are built around it.

Note that the current project is not ready for TypeScript, but I'm working on it. Because of this
I disabled TypeScript during build time like this:

/** @type {import('next').NextConfig} */


const nextConfig = {
experimental: {
appDir: true,
},

typescript: {
ignoreBuildErrors: false,
},

module.exports = nextConfig

Project Requirements
To follow along with this tutorial, you'll need basic knowledge of the following packages:

1. PNPM is a Node.js package manager similar to npm or yarn (you can use any of them
that you prefer).
2. TypeScript helps you write type-safe code in JavaScript, and can also help improve
productivity. It is not required, though. You can use JavaScript in your project.
3. React.js is a free and open-source front-end JavaScript library for building user
interfaces with class and function components.
4. Next.js 13 (app) is based on React and it provides additional functionality like routing,
error handling, and layouts.
5. Ghost CMS API is an open-source content management system (CMS) similar to
WordPress. Ghost is specifically designed and built for blogging. In this project, we'll
Ghost as the backend and Next as the frontend. For communication between the
backend and frontend development, we'll use the Ghost CMS API.
6. Tailwind CSS is an open source CSS-based framework similar to Bootstrap. We'll use
Tailwind CSS to design our blog website.

How to Set Up Ghost CMS


The next step is installing Ghost locally, which you can do with one command. First, you
need to install ghost-cli globally with pnpm, yarn, or npm.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 3/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

pnpm add -g ghost-cli@latest

# or

yarn global add ghost-cli@latest

# or

npm install ghost-cli@latest -g

After installing the Ghost CLI, you can create a new Ghost blog project locally with the
following command:

ghost install local

After the blog installation is finished, you can start your local development server with the
ghost start command and your local development serve on
http://localhost:2368/ghost.

Additional Ghost CLI Commands

There are a few additional commands that are helpful when using the Ghost CLI:

ghost start: start your server.


ghost stop : stop your running Ghost server.
ghost help : check the available list of commands.

Note:

Make sure your current installation directory is empty before installation. Currently, you are
installing Ghost in development mode. For production, you won't follow the same steps.

How to Set Up Ghost CMS with the Cloud


If you run into any problems with Ghost local installation, or maybe it's too complicated and
you do not have enough space on your drive, you can use a tool like digital press or any
other cloud service like GCP or AWS, Digital Ocean, and so on.

I like digital press because it comes with a free plan. Other cloud services do not provide
that, which is why I suggest it.

How to Get the Blog Template


Creating a new blog from scratch can be tough. In this tutorial, we'll use a pre-build template
from the frontend web. All templates have an open-source MIT license, so you can use them,
and you don't need to set up everything.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 4/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

I picked the Open-blog template from the frontend web.

How to Set Up Next.js


Setting up Next is one of the main parts of this tutorial, where you'll spend time and energy
coding, debugging, and deploying the site.

Here are the commands to run depending on whether you're using npx, yarn, or pnpm:

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

# or

yarn create next-app --experimental-app

# or

pnpm create next-app --experimental-app

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 5/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

create a new nextjs app.

After completing the installation process, we must install some additional Node packages for
the blog.

These Node packages can help you speed up your development process. Make sure to
install all the below packages to follow along with this guide:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 6/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Node packages to install:


1. pnpm add @tryghost/content-api(required)
2. pnpm add @types/tryghost__content-api (required by TypeScript)
3. pnpm add tailwindcss postcss autoprefixer
4. pnpm add @tailwindcss/typography
5. pnpm add react-icons
6. pnpm add date-fns
7. pnpm add next-themes
8. pnpm add @radix-ui/react-popover

Here's what each of these packages does:

@tryghost/content-api package is a Ghost JavaScript Client Library for fetching content


API data.
@types/tryghost__content-api package contains type definitions for @tryghost/content-
api.
TailwindCSS, autoprefixer, and PostCSS are packages required for Tailwind CSS.
@tailwindcss/typography package for handling dynamic typography with Tailwind CSS.
The next-themes package enables themes like switching from dark to light mode on
your site.
The react-icons package provides lots of SVG icons for the project. This way, you do
not need to download them manually.
@radix-ui/react-popover is part of the Radix UI ecosystem. I choose the Radix popover
component for the design of the search component on the site.
date-fns package helps convert your published_at date into a different date format.

What to Know Before Following This Tutorial


Before building this project, I highly recommend watching some tutorials on YouTube
(especially if you're a beginner with Next.js). These will help you understand some basics
about the Next.js experimental app folder.

Every video explains the same kind of topic. If you watch each of the four videos, you have a
basic idea of how the Next.js app folder works. That will make this advanced tutorial easier
to follow.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 7/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Vercel

Watch Video At: https://youtu.be/gSSsZReIFRk

In this tutorial, Lee Robinson covers the basics of routing, dynamic route segments, data
fetching, caching, and metadata.

Sakura Dev

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 8/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Watch Video At: https://youtu.be/6htDA6v4FPM


Sakura Dev teaches you about the difference between Next.js pages and the app folder and
routing with examples.

Tuomo Kankaanpaa

Watch Video At: https://youtu.be/xXwxEudjiAY

Tuomo Kankaanpaa teaches you about Next app folder routing, layouts, and server
components.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 9/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Piyush Garg

Watch Video At: https://youtu.be/CBfBZvDQLis

Piyush Garg compiles all the new Next features and converts them into a small crash
course, and builds a demo project.

Now that you're ready to go, let's get into building our blog.

Folder Structure
Our folder structure looks like this for our demo application:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 10/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

.
├── next.config.js
├── next-env.d.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── README.md
├── search.json
├── src
│ └── app
│ ├── authors
│ │ └── [slug]
│ │ └── page.tsx
│ ├── BlogLayout.tsx
│ ├── cards.min.css
│ ├── Card.tsx
│ ├── error.tsx
│ ├── favicon.ico
│ ├── Footer.tsx
│ ├── ghost-client.ts
│ ├── globals.css
│ ├── Header.tsx
│ ├── layout.tsx
│ ├── not-found.tsx
│ ├── pages
│ │ └── [slug]
│ │ └── page.tsx
│ ├── page.tsx
│ ├── pagination
│ │ └── [item]
│ │ └── page.tsx
│ ├── Pagination.tsx
│ ├── read
│ │ └── [slug]
│ │ ├── Newsletter.tsx
│ │ └── page.tsx
│ ├── Search.tsx
│ ├── SocialIcons.tsx
│ └── tags
│ └── [slug]
│ └── page.tsx
├── tailwind.config.js
└── tsconfig.json

13 directories, 30 files

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 11/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

How to Configure Ghost CMS and Next.js


The next step is to set up data fetching for the Ghost Content API. This is why we installed
the @tryghost/content-api package above.

Ghost CMS comes with two types of APIs: the first is the Content API, and the second is the
Admin API. For the blog, we'll use the Content API.

Content API is a RESTful API that fetches the published content for the Ghost database. It is
a read-only API. You can not call POST requests with it.

To configure it, we create a new file inside the src/app folder with ghost-client.ts. Inside
the file, we have a new Ghost API instance.

// ghost-client.ts

import GhostContentAPI from "@tryghost/content-api";

// Create API instance with site credentials


const api = new GhostContentAPI({
url: process.env.GHOST_URL as string,
key: process.env.GHOST_KEY as string,
version: "v5.0"
});

We need the blog URL, key, and version to config the Ghost content API in Next. You can
find both the URLs and Key properties in the Ghost dashboard, as well as the version value
which is your current version of Ghost CMS.

Go to the Ghost dashboard:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 12/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Get your KEY and URL

Go to dashboard > settings > integrations > Your-intergration-id and get your
GHOST_URL and GHOST_KEY . Now you can copy both and paste them inside your .env.local
file.

_Get your GHOST_KEY and GHOST_URL_

Understanding the Next.js 13 App Folder


There have been lots of changes in the Next.js pages folder and app folder with the release
of Next.js 13. We'll discuss some important stuff now and more when we're building the app:

1. There is no _app , _document, getServerSideProps, getStaticProps,


getStaticPaths , 404 and useRouter.
2. Now it combines the _app and _document files with the layout file.
3. useRouter is import from next/navigation.
4. The 404 file is replaced by the notFound() function.
5. The error.tsx file provides functionality like reacting to error boundaries.
6. Now the index.js file is replaced by page.js.
7. Passing dynamic route segments pages/blog/[slug].js is changed, and the Next
app directory looks like this: app/blog/[slug]/page.js.

Examples

To understand the Next experimental app folder, let's look at a real example:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 13/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

1. tag page => app/tag/[slug]/page.ts


2. category => app/tag/[slug]/page.ts

Now you can create five files inside every route. For example, if you create a tag or
category route in your app folder, then you can create four files inside your app route folder.

page.ts (required): it is your main file.


layout.ts (optional): it helps design your layout
loading.ts (optional): it creates a loading indicator with React suspense.
error.ts (optional): it helps handle errors in your React app.
components (optional): you can also create another component in your routes.

Let's understand how the new Next.js 13 app route works with a real-life example: your tag
route folder looks like this.

app/tag/[slug]/page.ts
app/tag/[slug]/loading.ts
app/tag/[slug]/layout.ts
app/tag/[slug]/error.ts
app/tag/[slug]/my-card-component.ts

Demo Data for the Project


You don't have to worry about creating a demo or dummy blog post data. For your testing,
You can download it from this GitHub repository.

How to Build the Blog


We'll go through and build each part of the blog in the following sections so you can follow
along at home.

How to Build the Header


The first and main part of the site is the header. First, we'll create a simple header for our
demo blog. Our header will end up looking like this:

Design of the header

First is the logo, next comes the nav bar with various elements, and last is the icon section.
All the data comes from the Ghost CMS API. You can change things inside Ghost CMS and
it will reflect on the site.

Here's the code to build the header component:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 14/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// Header.tsx

import Link from "next/link";


import SocialIcons from "./SocialIcons";
import Image from "next/image";
import type { Settings } from "@tryghost/content-api";

function Header({ setting }: { setting: Settings }) {

return (
<header className="px-2 sm:px-4 py-2.5 dark:bg-gray-900 w-full">

<div className="container flex flex-wrap items-center justify-between mx-auto">


{/* Logo for blog */}
<Link href="/" className="flex items-center">
{setting.logo !== null ?
<Image
alt={setting.title} width={200} height={100} src={setting.logo}
className="self-center text-xl font-semibold whitespace-nowrap dark:text-white" />
: setting.title}
</Link>
<div className="flex md:order-2">

<ul className="flex flex-wrap p-4 md:space-x-8 md:mt-0 md:text-sm md:font-


medium">

{
/* Blog Navigation Edit in GHOST CMS */
setting.navigation !== undefined ? setting?.navigation.map(item => <li
key={item.label} className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:text-
blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white"
aria-current="page">
<Link href={item.url}>
{item.label}
</Link>
</li>) : " "

</ul>

</div>
<SocialIcons setting={setting} />
</div>

</header >
)

}
export default Header

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 15/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

How to Build the Footer


The footer is also an important section of a blog site. It shows your important information and
various helpful links.

Design of the footer

I designed a basic footer with copyrighted text and added social icons for the site. The social
icons come from the Ghost CMS API.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 16/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// Footer.tsx

import { FaTwitter, FaFacebook } from "react-icons/fa";


import Link from "next/link";
import type { Settings } from "@tryghost/content-api";

function Footer({ setting }: { setting: Settings }) {

return (

<footer className="px-2 sm:px-4 py-2.5 dark:bg-gray-900 w-full">

<div className="container flex flex-wrap items-center justify-between mx-auto">

<Link href="https://github.com/frontendweb3" className="flex items-center">


<span className="self-center text-gray-800 text-sm font-semibold
whitespace-nowrap dark:text-white">2023 copyright frontend web</span>
</Link>

<div className="flex md:order-2">

<ul className="flex p-4 flex-row md:space-x-8 md:mt-0 md:text-sm font-


medium">

{
setting.twitter !== null ? <li>
<Link target="_blank" href={`https://twitter.com/${setting.twitter}`}
className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:text-blue-700
dark:hover:text-blue-700 md:p-0 dark:text-white" aria-current="page">
<FaTwitter />
</Link>
</li> : " "

{
setting.facebook !== null ? <li>
<Link target="_blank" href=
{`https://www.facebook.com/${setting.facebook}`} className="block py-2 pl-3 pr-4
text-gray-700 rounded hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-
white ">
<FaFacebook />
</Link>
</li> : " "

</ul>
</div>

</div>

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 17/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

</footer>

)
}

export default Footer

How to Build the Layout

I designed a basic layout for the blog. For building layouts in Next.js, there's a special
layout.tsx file.

Before we create the layout design, we need to define a getNavigation function to fetch
navigation and basic website-related data from Ghost.

// ghost-client.ts

export async function getNavigation() {


return await api.settings.browse()
}

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 18/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

The data look like this:

{
title: 'Rajdeep Singh',
description: 'Thoughts, stories and ideas.',
logo: 'http://localhost:2368/content/images/2023/04/nextjsandghostlogo-2.png',
icon: 'http://localhost:2368/content/images/size/w256h256/2023/04/nextjs-
60pxx60px.png',
accent_color: '#d27fa0',
cover_image: 'https://static.ghost.org/v4.0.0/images/publication-cover.jpg',
facebook: 'ghost',
twitter: '@ghost',
lang: 'en',
locale: 'en',
timezone: 'Etc/UTC',
codeinjection_head: null,
codeinjection_foot: null,
navigation: Array(5) [
{ label: 'Home', url: '/' }, { label: 'JavaScript', url: '/tags/javascript/' }, {
label: 'Nextjs', url: '/tags/nextjs/' },
{ label: 'Reactjs', url: '/tags/reactjs/' }, { label: 'Ghost CMS', url:
'/tags/ghost-cms/' }
],
secondary_navigation: Array(1) [ { label: 'Login', url: '#/portal/' } ],
meta_title: 'My demo post',
meta_description:
'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem
Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an
unknown printer took a galley of type and scrambled it to make a type specimen
book.',
og_image: null,
og_title: null,
og_description:
'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem
Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an
unknown printer took a galley of type and scrambled it to make a type specimen
book.',
twitter_image: null,
twitter_title: null,
twitter_description:
'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem
Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an
unknown printer took a galley of type and scrambled it to make a type specimen
book.',
members_support_address: 'noreply',
members_enabled: true,
members_invite_only: false,
paid_members_enabled: false,
firstpromoter_account: null,
portal_button_style: 'icon-and-text',
portal_button_signup_text: 'Subscribe',

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 19/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

portal_button_icon: null,
portal_plans: Array(1) [ 'free' ],
portal_name: true,
portal_button: true,
comments_enabled: 'all',
url: 'http://localhost:2368/',
version: '5.39'
}

The getNavigation function returns the settings data, and then we pass the data as props
into the header and footer components.

Our Main layout.tsx file works server side. It helps fetch data on the server side with the
React use hook.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 20/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// Layout.tsx

import "./globals.css";
import BlogLayout from './BlogLayout'
import { getNavigation, } from "./ghost-client"
import { use } from "react"
import type { Settings } from "@tryghost/content-api"

interface UpdateSettings extends Settings {


accent_color?: string;
}

export default function RootLayout({


children,
}: {
children: React.ReactNode
}) {

const settings: UpdateSettings = use(getNavigation())

return (

<html className='light' lang="en">

<body
style={{
'--bg-color': settings?.accent_color ? settings.accent_color : "",
}}
className={` bg-[--bg-color] dark:bg-gray-900`}>

<BlogLayout setting={settings}>

{children}

</BlogLayout>

</body>

</html>

)
}

BlogLayout component

The BlogLayout component works on the client side. In the Next.js app folder, you can
easily convert your server-side component to the client side with the following "use client"
syntax.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 21/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

The purpose of the BlogLayout component is to contain the ThemeProvider, header, and
footer. ThemeProvider is a high-order component, and it provides additional functionality, like
changing the theme from dark to light. We wrap the intra-site with ThemeProvider's higher
component. In the old pages directory, we achieve similarly functionally with nextjs _app.ts
custom app.

ThemeProvider component helps to change the theme from light to dark mode.

"use client"

// BlogLayout.tsx

import Footer from "./Footer";


import Header from "./Header";
import { ThemeProvider } from 'next-themes';
import type { Settings } from "@tryghost/content-api";
function Layout({ setting, children }: { setting: Settings, children: React.ReactNode
}) {
return <ThemeProvider attribute="class">
<Header setting={setting} />
{children}
<Footer setting={setting} />
</ThemeProvider>

}
export default Layout

How to Build the Homepage

Next.js has a special app/page.tsx file for designing and building the home page. Our blog
website's home page looks like what you see below. We import the header, card, pagination,
and footer on the home page. The header and footer are part of layout.tsx.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 22/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Home page

First, we fetch all posts data from Ghost CMS with the help of the getPosts function, which I
defined in the ghost-client.ts file.

// ghost-client.ts

export async function getPosts() {


return await api.posts
.browse({
include: ["tags", "authors"],
limit: 10
})
.catch(err => {
throw new Error(err)
});
}

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 23/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

By default, the api.post.browse() returns only post data, but you can easily extend it. In
every article or post data, we also include tags and authors with the help of include. Then
we set the article limit to ten.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 24/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

The data look like this:

[
{
id: '6422a742136f5d40f37294f5',
uuid: '8c2fcfda-a6e4-4383-893b-ba18511c0f67',
title: 'Demo Posts with Nextjs and Ghost Editor',
slug: 'demo-posts-with-nextjs-and-reactjs',
html: `<p><strong>Lorem Ipsum</strong> is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industry's standard dummy text si
nce the 1500s when an unknown printer scrambled a galley of type and scrambled it to
make a type specimen book. </p><p>It has survived five centuries and the leap i
nto electronic typesetting, remaining essentially unchanged. </p><p>It was
popularised in the 1960s with Letraset sheets containing Lorem Ipsum passages and,
more r
ecently, desktop publishing software like Aldus PageMaker, including versions of
Lorem Ipsum.</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascap
tion"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-
gallery-image"><img src="http://localhost:2368/content/images/2023/03/Build-and-d
eploy.png" width="1500" height="400" loading="lazy" alt
srcset="http://localhost:2368/content/images/size/w600/2023/03/Build-and-deploy.png
600w, http://localhost:2
368/content/images/size/w1000/2023/03/Build-and-deploy.png 1000w,
http://localhost:2368/content/images/2023/03/Build-and-deploy.png 1500w" sizes="(min-
width: 720px)
720px"></div><div class="kg-gallery-image"><img
src="http://localhost:2368/content/images/2023/03/Build-and-deploy-profile-1.png"
width="1500" height="400" loading
="lazy" alt srcset="http://localhost:2368/content/images/size/w600/2023/03/Build-and-
deploy-profile-1.png 600w, http://localhost:2368/content/images/size/w1000/2023
/03/Build-and-deploy-profile-1.png 1000w,
http://localhost:2368/content/images/2023/03/Build-and-deploy-profile-1.png 1500w"
sizes="(min-width: 720px) 720px"></div>
</div><div class="kg-gallery-row"><div class="kg-gallery-image"><img
src="http://localhost:2368/content/images/2023/03/Build-and-deploy-profile--1--1.png"
width="15
00" height="400" loading="lazy" alt
srcset="http://localhost:2368/content/images/size/w600/2023/03/Build-and-deploy-
profile--1--1.png 600w, http://localhost:2368/co
ntent/images/size/w1000/2023/03/Build-and-deploy-profile--1--1.png 1000w,
http://localhost:2368/content/images/2023/03/Build-and-deploy-profile--1--1.png
1500w" siz
es="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img
src="http://localhost:2368/content/images/2023/03/Build--Test-and-Deploy-profile-
1.png" width
="1500" height="400" loading="lazy" alt
srcset="http://localhost:2368/content/images/size/w600/2023/03/Build--Test-and-
Deploy-profile-1.png 600w, http://localhost:2
368/content/images/size/w1000/2023/03/Build--Test-and-Deploy-profile-1.png 1000w,
http://localhost:2368/content/images/2023/03/Build--Test-and-Deploy-profile-1.png
1500w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption>Build and

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 25/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

deploy</figcaption></figure><h2 id="why-do-we-use-it">Why do we use it?</h2><p>It is


a long-established fact that a reader will be distracted by the readable content of
a page when looking at its layout. </p><p>The point of using Lorem Ipsum is tha
t it has a more-or-less normal distribution of letters, as opposed to using 'Content
here, content here', making it look like readable English. </p><p>Many desktop
publishing packages and web page editors now use Lorem Ipsum as their default model
text, and a search for 'lorem ipsum' will uncover many web sites still in their
infancy. </p><p>Various versions have evolved over the years, sometimes by accident,
sometimes on purpose (injected humour and the like).</p><hr><h2 id="where-can-i
-get-some">Where can I get some?</h2><p>There are many variations of passages of
Lorem Ipsum available, but the majority have suffered alteration in some form, by i
njected humour, or randomised words which don't look even slightly believable. </p>
<p>If you are going to use a passage of Lorem Ipsum, you need to be sure there is
n't anything embarrassing hidden in the middle of text. </p><p>All the Lorem Ipsum
generators on the Internet tend to repeat predefined chunks as necessary, making
this the first true generator on the Internet. </p><p>It uses a dictionary of over
200 Latin words, combined with a handful of model sentence structures, to generat
e Lorem Ipsum which looks reasonable. </p><p>The generated Lorem Ipsum is therefore
always free from repetition, injected humour, or non-characteristic words etc.</
p><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-
💡
emoji"> </div><div class="kg-callout-text">My note is here&nbsp;</div></div><p></
p><div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style
data-kg-background-image><h2 class="kg-header-card-header" id="product">Produc
t</h2><h3 class="kg-header-card-subheader" id="my-blog-list">My blog list</h3></div>
<p></p><figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="2
00" height="113" src="https://www.youtube.com/embed/_q1K7cybyRk?feature=oembed"
frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gy
roscope; picture-in-picture; web-share" allowfullscreen title="Next.js 13.1
Explained!"></iframe><figcaption>youtube</figcaption></figure><hr><figure class="kg-
card
kg-embed-card"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">In 2022, we
enabled developers to create at the moment of inspiration, now with over 2 mill
ion deployments per week.<br><br>Here&#39;s what we shipped ↓ <a
href="https://t.co/6k7Xmbpna3?ref=localhost">pic.twitter.com/6k7Xmbpna3</a>
</p>&mdash; Vercel (@ver
cel) <a href="https://twitter.com/vercel/status/1611094825587167254?
ref_src=twsrc%5Etfw&ref=localhost">January 5, 2023</a></blockquote>\n` +
'<script async src="https://platform.twitter.com/widgets.js" charset="utf-8">
</script>\n' +
'</figure><hr><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a
class="kg-bookmark-container" href="https://medium.com/frontendweb/what-is-progre
ssive-web-app-and-how-to-enable-it-in-nextjs-application-17f2e3240390?ref=localhost">
<div class="kg-bookmark-content"><div class="kg-bookmark-title">What is Progres
sive Web App and How to enable it in nextjs Application?</div><div class="kg-
bookmark-description">A detailed guide to Progressive Web Apps: How to use it with
next
js and publish on Google play store, Microsoft store, Meta Quest, and…</div><div
class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://cdn-static-
1.medium.com/_/fp/icons/Medium-Avatar-500x500.svg" alt><span class="kg-bookmark-
author">FrontEnd web</span><span class="kg-bookmark-publisher">Rajdeep singh</span><
/div></div><div class="kg-bookmark-thumbnail"><img

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 26/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

src="https://miro.medium.com/v2/resize:fit:1200/1*yAoHfq4Wm2Bp8DU1Dav29Q.png" alt>
</div></a><figcaption>Bookmark<
/figcaption></figure><div class="kg-card kg-header-card kg-width-full kg-size-small
kg-style-dark" style data-kg-background-image><h2 class="kg-header-card-header"
id="thank-you">Thank you</h2></div>',
comment_id: '6422a742136f5d40f37294f5',
feature_image: 'https://images.unsplash.com/photo-1543966888-7c1dc482a810?
crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE2fHxqYXZhc2Ny
a
XB0fGVufDB8fHx8MTY3OTk5MjY1NA&ixlib=rb-4.0.3&q=80&w=2000',
featured: false,
visibility: 'public',
created_at: '2023-03-28T08:37:22.000+00:00',
updated_at: '2023-03-28T08:51:38.000+00:00',
published_at: '2023-03-28T08:50:44.000+00:00',
custom_excerpt: 'It has survived five centuries and the leap into electronic
typesetting, remaining essentially unchanged. ',
codeinjection_head: null,
codeinjection_foot: null,
custom_template: null,
canonical_url: null,
tags: [ [Object] ],
authors: [ [Object] ],
primary_author: {
id: '1',
name: 'Rajdeep Singh',
slug: 'rajdeep',
profile_image:
'https://www.gravatar.com/avatar/dafca7497609ae294378279ad1d6136c?s=250&r=x&d=mp',
cover_image: null,
bio: 'Lorem Ipsum is simply dummy text of the printing and typesetting
industry. ',
website: 'https://officialrajdeepsingh.dev',
location: 'India',
facebook: 'officialrajdeepsingh',
twitter: '@Official_R_deep',
meta_title: null,
meta_description: null,
url: 'http://localhost:2368/author/rajdeep/'
},
primary_tag: {
id: '6422aa9a136f5d40f3729552',
name: 'demo',
slug: 'demo',
description: null,
feature_image: null,
visibility: 'public',
og_image: null,
og_title: null,
og_description: null,
twitter_image: null,

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 27/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

twitter_title: null,
twitter_description: null,
meta_title: null,
meta_description: null,
codeinjection_head: null,
codeinjection_foot: null,
canonical_url: null,
accent_color: null,
url: 'http://localhost:2368/tag/demo/'
},
url: 'http://localhost:2368/demo-posts-with-nextjs-and-reactjs/',
excerpt: 'It has survived five centuries and the leap into electronic
typesetting, remaining essentially unchanged. ',
reading_time: 3,
access: true,
comments: true,
og_image: null,
og_title: null,
og_description: null,
twitter_image: null,
twitter_title: null,
twitter_description: null,
meta_title: null,
meta_description: null,
email_subject: null,
frontmatter: null,
feature_image_alt: 'Demo Posts with Nextjs and Ghost Editor',
feature_image_caption: 'Photo by <a href="https://unsplash.com/@pinjasaur?
utm_source=ghost&utm_medium=referral&utm_campaign=api-credit">Paul Esch-Laurent</a> /
<a href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-
credit">Unsplash</a>'
},
meta:{
pagination: { page: 1, limit: 10, pages: 2, total: 12, next: 2, prev: null }
}
]

Now we call the getPosts function on the server side. It returns all the post data with the
associated tags and authors. Now you can loop through the data with a map() function.

We pass the data into app/page.tsx to the card.tsx components. We pass the article data
as props into the card component.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 28/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/page.tsx

import { getPosts } from "./ghost-client"


import Card from './Card'

export default async function Home() {

const getPost = await getPosts()

return (
<>
<main className="container my-12 mx-auto grid grid-cols-1 gap-2 md:gap-3
lg:gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4">

{
getPost?.map(
item => {
return <Card key={item.uuid} item={item} />
})
}

</main>

</>
)
}

Card component

I designed a basic card for the blog. The card component looks like this:

Card component

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 29/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

I rendered every item of data coming from the home page as props and showed it on the site
with Card.tsx .

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 30/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// Card.tsx

import Image from "next/image"


import Link from "next/link"
import type { PostOrPage } from "@tryghost/content-api";
import { format } from 'date-fns'

function Card({ item }: { item: PostOrPage }) {

return (
<div className="max-w-full bg-white dark:bg-gray-800" >

{
item.featured !== null && item.feature_image !== undefined ? <Link href=
{`/read/${item.slug}`}>
<Image className="rounded-lg p-3" width={1000} height={324} src=
{item.feature_image} alt={item.feature_image_alt || item.title} />
</Link> : " "
}

<div className="p-3">

<div className="flex mb-3">


{
item.published_at !== null && item.published_at !== undefined ? <p
className="text-sm text-gray-500 dark:text-gray-400">
{format(new Date(item.published_at), 'dd MMMM, yyyy')}
</p> : ""
}
<p className="text-sm text-gray-500 dark:text-gray-400 mx-1"> , </p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{item.reading_time} min read
</p>
</div>

<Link href={`/read/${item.slug}`}>
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900
dark:text-white">
{item.title}
</h5>
</Link>

</div>

</div>

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 31/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

export default Card

How to Build the Reading Page


The reading page is the second most important page for the blog site. If people can't figure
out how to read what the author writes, this is a big problem for front-end developers.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 32/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 33/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Reading page

First, we get a single article from the Ghost CMS API based on its slug. We pass it to the
Card component with the Link component.

// ghost-client.ts

export async function getSinglePost(postSlug: string) {


return await api.posts
.read({
slug: postSlug
}, { include: ["tags", "authors"] })
.catch(err => {
console.error(err);
});
}

The getSinglePost(<you-slug>) function returns data about a single article, and you can
render that data on the page.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 34/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/read/[slug]/page.tsx

import Newsletter from "./Newsletter";


import Link from "next/link";
import { getSinglePost, getPosts } from "../../ghost-client"
import Image from "next/image";
// import icon
import { FaAngleLeft } from "react-icons/fa";

// types for typescript


import type { Metadata } from "next";
import type { PostOrPage } from "@tryghost/content-api";

// format the date


import { format } from 'date-fns'

// css for card


import "../../cards.min.css"

export async function generateStaticParams() {


const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}));
}

async function Read({ params }: { params: { slug: string }; }) {

const getPost = await getSinglePost(params.slug)

return (
<>
<main className="pt-8 pb-16 lg:pt-16 lg:pb-24 dark:bg-gray-900">

<div className="flex justify-between px-4 mx-auto max-w-screen-xl ">

<article className="mx-auto w-full max-w-3xl prose prose-xl prose-p:text-


gray-800 dark:prose-p:text-gray-100 sm:prose-base prose-a:no-underline prose-blue
dark:prose-invert">

<div className="flex mb-4 w-full justify-between">

<Link className="inline-flex items-center" href={`/`}>


<FaAngleLeft /> Back
</Link>

{
getPost.primary_tag ? <Link href=
{`/tags/${getPost?.primary_tag.slug}`}>

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 35/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

# {getPost?.primary_tag.name}
</Link> : ""
}

</div>

<h1 className="mb-4 text-3xl font-extrabold leading-tight text-gray-900


lg:mb-6 lg:text-4xl dark:text-white">
{getPost.title}
</h1>

<p className="lead">
{getPost.excerpt}
</p>

<header className="mb-4 lg:mb-6 not-format">

<address className="flex items-center mb-6 not-italic">

<div className="inline-flex items-center mr-3 text-sm text-gray-900


dark:text-white">

<Image width={32} height={32} className="mr-4 w-10 h-10 rounded-


full" src={getPost?.primary_author.profile_image} alt={getPost?.primary_author.name}
/>
{
getPost.primary_author ? <Link href=
{`/authors/${getPost?.primary_author.slug}`} rel="author" className="text-xl font-
bold text-gray-800 dark:text-white">{getPost?.primary_author.name}</Link> : " "
}

{
getPost.published_at ? <time className="text-base font-light
text-gray-800 dark:text-white mx-1" dateTime={getPost?.published_at} title=
{format(new Date(getPost?.published_at), 'yyyy-MM-dd')}>
{format(new Date(getPost?.published_at), 'dd MMMM, yyyy')}
</time> : ""
}

<div className="text-base w-1 h-1 rounded-full bg-black dark:bg-


white mx-1"></div>

<p className="text-base font-light text-gray-500 dark:text-gray-


400"> {getPost.reading_time} Min Read</p>

</div>

</address>

</header>

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 36/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

<figure>
<Image className="mx-auto" width={1000} height={250} src=
{getPost.feature_image} alt={getPost.feature_image_alt} />
<figcaption className="text-center"
dangerouslySetInnerHTML={{
__html: getPost?.feature_image_caption
}}></figcaption>
</figure>

<div dangerouslySetInnerHTML={{ __html: getPost?.html }}></div>

</article>
</div>
</main>
<Newsletter />
</>
)

}
export default Read

You render the post's HTML data with dangerouslySetInnerHTML . But you need to write
lots of CSS to handle the dynamic content coming from the Ghost CMS API.

To solve that, I used the @tailwindcss/typography package. I also downloaded


cards.min.css from Ghost. Now you don't need to write a single line of CSS in your Next
app.

Generate the static site with the generateStaticParams function. Before, we used to
getStaticProps function.

// ghost-client.ts

export async function generateStaticParams() {

// fetch All posts

const posts = await getPosts()

// return the slug

return posts.map((post) => ({


slug: post.slug,
}));

How to Build the Tag Page

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 37/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

I designed a simple tag page for the blog. The tag page shows articles related to the tags
that are used.

You can also create a category page. Tag pages and category pages use the same logic and
functionalities.

Tag page

Similar to the reading page, we'll get articles based on tags from the Ghost CMS API.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 38/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// ghost-client.ts

// return all posts realted to tag slug


export async function getTagPosts(tagSlug: string) {

return await api.posts.browse({ filter: `tag:${tagSlug}`, include: 'count.posts' })


.catch(err => {
throw new Error(err)
});
;

// return all the slugs to build static with generateStaticParams


export async function getAllTags() {
return await api.tags.browse({
limit: "all"
}).catch(err => {
console.log(err)
})
}

The getTagPosts(<tag-slug>) function returns all the available posts related to a specific
tag.

After receiving all posts with getTagPosts(), we render all posts with the help of the map()
method.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 39/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/tag/[slug]/page.tsx

import React from 'react'


import Card from "../../Card"

import { getTagPosts, getAllTags } from "../../ghost-client"

import { notFound } from 'next/navigation';

import type { PostsOrPages } from "@tryghost/content-api";

export async function generateStaticParams() {

const allTags: Tags = await getAllTags()

let allTagsItem: { slug: string }[] = []

// genrate the slug for static site

allTags?.map(item => {
allTagsItem.push({
slug: item.slug,
})
})

return allTagsItem

async function Tag({ params }: { params: { slug: string }; }) {

let tagPosts: PostsOrPages = await getTagPosts(params.slug)

// Handling 404 error

if (tagPosts.length === 0) {
notFound()
}

return (
<aside aria-label="Related articles" className="py-8 lg:py-24 dark:bg-gray-800">

<div className="px-4 mx-auto max-w-screen-xl">

<h2 className="mb-8 text-2xl font-bold text-gray-900 dark:text-white">


More articles from {params.slug.split("-").join(" ")}
</h2>

<div className="container my-12 mx-auto grid grid-cols-1 gap-12 md:gap-12

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 40/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

lg:gap-12 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4 ">

{
tagPosts.map(
item => <Card key={item.uuid} item={item} />
)
}

</div>

</div>

</aside>
)

export default Tag

Generate the static site with the generateStaticParams function. It helps to generate slugs
of the static build.

// ghost-client.ts

export async function getAllTags() {


return await api.tags.browse({
limit: "all"
}).catch(err => {
console.log(err)
})
}

How to Build the Author Page


The last and one of the most important pages for the blog site is the author page. This is
where readers can learn more about the author.

For the demo blog, I designed a basic page for the author.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 41/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Author page

We'll build this in a similar way as we built the tag page. First, we get the author's metadata
and author posts from the Ghost CMS API.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 42/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// ghost-client.ts

// get author meta Data

export async function getSingleAuthor(authorSlug: string) {


return await api.authors
.read({
slug: authorSlug
}, { include: ["count.posts"] })
.catch(err => {
console.log(err)
});

// get author related posts

export async function getSingleAuthorPosts(authorSlug: string) {


return await api.posts.browse({ filter: `authors:${authorSlug}` })
.catch(err => {
console.log(err)
})
};

// get All author from Ghost CMS for generateStaticParams

export async function getAllAuthors() {

return await api.authors


.browse({
limit: "all"
})
.catch(err => {
throw new Error(err)
});

The getSingleAuthor(<author-slug>) returns data about a single author based on the


author slug, and the getSingleAuthorPosts(<author-slug>) function returns all posts
related to the author.

We render the posts data with the help of the map() method.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 43/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/author/[slug]/page.tsx

import React from 'react';


import Link from "next/link";
import { FaFacebook, FaTwitter, FaGlobe } from "react-icons/fa";
import Card from "../../Card"

import { getSingleAuthor, getSingleAuthorPost, getAllAuthors } from "../../ghost-


client"

import Image from 'next/image';


import { notFound } from 'next/navigation';

import type { Author, PostsOrPages } from "@tryghost/content-api";

export async function generateStaticParams() {

const allAuthor: Author[] = await getAllAuthors()

let allAuthorItem: { slug: string }[] = []

allAuthor.map(item => {
allAuthorItem.push({
slug: item.slug,
})
})
return allAuthorItem

async function AuthorPage({ params }: { params: { slug: string }; }) {

const getAuthor: Author = await getSingleAuthor(params.slug)

const allAuthor: PostsOrPages = await getSingleAuthorPost(params.slug)

// Handling 404 errors


if (allAuthor?.length === 0) {
notFound()
}

return (
<>
<section className="dark:bg-gray-900">
<div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">

<div className=" p-10 text-gray-500 sm:text-lg dark:text-gray-400">

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 44/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

{
getAuthor?.profile_image !== undefined ? <Image height={30} width={30}
className="w-36 h-36 p-2 rounded-full mx-auto ring-2 ring-gray-300 dark:ring-gray-
500" src={getAuthor?.profile_image} alt={getAuthor?.name} /> : ""
}

{
getAuthor?.name ? <h2 className="mb-4 mt-4 text-4xl tracking-tight
font-bold text-center text-gray-900 dark:text-white">
{getAuthor?.name.split(" ")[0]}
<span className="font-extrabold">
{getAuthor?.name?.split(" ")[1]}
</span>
</h2> : ""
}

<p className="mb-4 font-light text-center">{getAuthor?.bio} </p>

<ul className="flex flex-wrap p-4 justify-center md:space-x-8 md:mt-0


md:text-sm md:font-medium">

{
(getAuthor?.website !== null) ? (<li>
<Link href={getAuthor?.website} className="block py-2 pl-3 pr-4
text-gray-700 hover:text-blue-700 dark:hover:text-blue-700 rounded md:p-0 dark:text-
white" aria-current="page">
<FaGlobe />
</Link> </li>) : " "

{
(getAuthor?.twitter !== null) ? (<li>
<Link href={getAuthor?.twitter} className="block py-2 pl-3 pr-4
text-gray-700 rounded hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-
white" aria-current="page">
<FaTwitter />
</Link>
</li>) : " "
}

{
(getAuthor?.facebook !== null && getAuthor.facebook !== undefined) ?
(<li>
<Link href={getAuthor?.facebook}
className="block py-2 pl-3 pr-4 text-gray-700 rounded
hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white"> <FaFacebook />
</Link>

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 45/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

</li>) : " "

</ul>

</div>
</div>
</section>

<aside aria-label="Related articles" className="py-8 lg:py-24 dark:bg-gray-


800">
<div className="px-4 mx-auto max-w-screen-xl">

<h2 className="mb-8 text-2xl font-bold text-gray-900 dark:text-white">


More articles from {getAuthor?.name}
</h2>

<div className="container my-12 mx-auto grid grid-cols-1 gap-12 md:gap-12


lg:gap-12 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4 ">

{
allAuthor?.map(item => <Card key={item?.uuid} item={item} />)
}

</div>
</div>
</aside>

</>
)

}
export default AuthorPage

To generate the author slug for the static site, we need to use the generateStaticParams
function. We do not need anything else to build the static site.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 46/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// ghost-client.ts

// Build Static Site

export async function generateStaticParams() {

const allAuthor: Author[] = await getAllAuthors()

let allAuthorItem: { slug: string }[] = []

allAuthor.map(item => {
allAuthorItem.push({
slug: item.slug,
})
})
return allAuthorItem

How to Build Single Pages


For single pages like About, Contact, Privacy Policy, and so on, you can also create them
with the Ghost Content API.

Our single-page design looks like this:

single blog page

Firstly, you need to fetch all pages and the single pages data from the Ghost Content API.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 47/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// ghost-client.tsx

// fetch all pages

export async function getSinglePage(pageSlug: string) {


return await api.pages
.read({
slug: pageSlug
})
.catch(err => {
console.error(err);
});
}

// single page data

export async function getSinglePage(pageSlug: string) {


return await api.pages
.read({
slug: pageSlug
}, { include: ["tags"] })
.catch(err => {
console.error(err);
});
}

The getSinglePage(page-slug) function returns the single page data based on the page
slug, and the getAllPages() function returns all the available published page data to
generate the dynamic params with the generateStaticParams() function.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 48/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/pages/[slug]/page.tsx

import { getSinglePage, getAllPages } from "../../ghost-client"


import { notFound } from 'next/navigation';
import type { PostOrPage } from "@tryghost/content-api";
import "../../cards.min.css"

// genrate Static slug or params for blog

export async function generateStaticParams() {


const pages = await getAllPages()

return pages.map((post) => ({


slug: post.slug,
}));
}

async function Pages({ params }: { params: { slug: string }; }) {

// fetch single page


const getPage = await getSinglePage(params.slug)

// handle 404 error


if (!getPage) {
notFound()
}

return (
<>
<main className="pt-8 pb-16 lg:pt-16 lg:pb-24 dark:bg-gray-900">
<div className="flex justify-between px-4 mx-auto max-w-screen-xl ">

<article className="mx-auto w-full max-w-3xl prose prose-xl prose-p:text-


gray-800 dark:prose-p:text-gray-100 sm:prose-base prose-a:no-underline prose-blue
dark:prose-invert">

<h1 className="mb-14 text-3xl font-extrabold leading-tight text-gray-900


lg:mb-6 lg:text-4xl dark:text-white">
{getPage.title}
</h1>

<div dangerouslySetInnerHTML={{ __html: getPage?.html }}></div>

</article>
</div>
</main>
</>
)

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 49/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

}
export default Pages

How to Handle Pagination


Pagination helps speed up your site as well as divide your site into smaller parts, more
digestible pages. You can link your posts with each other with prev and next.

meta:{
pagination: { page: 1, limit: 10, pages: 2, total: 12, next: 2, prev: null }
}

Firstly, we'll create a Pagination.tsx file as a React component.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 50/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// Pagination.tsx

import Link from "next/link"


import { Pagination } from "@tryghost/content-api"

function PaginationItem({ item }: { item: Pagination }) {

let paginationItems = []

for (let index = 1; index <= item?.pages; index++) {


paginationItems.push(<li key={index * 2} ><Link href={index === 1 ? "/" :
`/pagination/${index}`} className="px-3 py-2 leading-tight bg-blue-100 hover:bg-blue-
200 border-transparent border rounded-lg text-black dark:bg-gray-800 dark:text-gray-
400 mx-2 dark:hover:bg-gray-700 dark:hover:text-white">
{index}
</Link></li>)
}

return (

<nav aria-label="pagination" className="mx-auto my-20 container">

<ul className="mx-auto flex justify-center -space-x-px">

<li>
{
item.prev ? <Link href={item.prev === 1 ? "/" :
`/pagination/${item.prev}`} className="px-3 py-2 mr-2 border border-transparent
rounded-md leading-tight bg-white hover:text-blue-700 dark:bg-gray-800 dark:text-
gray-400
dark:hover:bg-gray-700 dark:hover:text-white">
Prev
</Link> : " "
}
</li>

{paginationItems}

<li>
{
item.next ? <Link href={`/pagination/${item.next}`} className="px-3 py-2
ml-2 border border-transparent rounded-md leading-tight bg-white hover:text-blue-700
dark:bg-gray-800 dark:text-gray-400
dark:hover:bg-gray-700 dark:hover:text-white">
Next
</Link> : " "
}
</li>

</ul>

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 51/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

</nav>

export default PaginationItem

When you call the api.posts.browse({ limit: 10 }) request, the API endpoint returns
ten posts and a meta object with pagination.

The returned api.posts.browse({ limit: 10 }) data look like this:

[
{title: 'Demo Posts with Nextjs and Ghost Editor',... },
{title: Trigger the hook and rebuild the nextjs site',... }

meta:{
pagination: { page: 1, limit: 10, pages: 2, total: 12, next: 2, prev: null }
}
]

Now based on meta, we can create pagination and pass meta.pagination as props to the
Pagination component.

// src/app/page.tsx

import { getPosts } from "./ghost-client"


import Pagination from "./Pagination"

export default async function Home() {

const getPost = await getPosts()

const AllPostForSerach = await getSearchPosts()

return (
<>
{/* rest of code */}
<Pagination item={getPost.meta.pagination} />
</>
)
}

To enable dynamic pagination, we'll create a src/app/pagination/[item]/page.tsx route


in the blog. You can use whatever name you want for the pagination route.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 52/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// ghost-client.tsx

// return all posts for generateStaticParams

export async function getPosts() {


return await api.posts
.browse({
include: ["tags", "authors"],
limit: 10
})
.catch(err => {
throw new Error(err)
});
}

//
export async function getPaginationPosts(page: number) {
return await api.posts
.browse({
include: ["tags", "authors"],
limit: 10,
page: page
})
.catch(err => {
throw new Error(err)
});
}

The getPosts is used to render the Pagination component on the pagination page. The
important part is the getPaginationPosts(<pagination-page-number>) function, which
returns posts based on the pagination page number.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 53/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/pagination/[item]/page.tsx

import { getPaginationPosts, getPosts } from "../../ghost-client"


import Card from '../../Card'
import PaginationItem from "../../Pagination"
import type { Metadata } from "next";
import type { PostsOrPages } from "@tryghost/content-api";

export async function generateStaticParams() {

const posts:PostsOrPages = await getPosts()

let paginationItem: { item: number }[] = []

for (let index = 1; index <= posts?.meta.pagination.pages; index++) {


paginationItem.push({
item: index,
})

return paginationItem

export default async function Pagination({ params }: { params: { item: string }; }) {

let getParams: number = Number.parseInt(params.item)

const getPost: PostsOrPages = await getPaginationPosts(getParams)

return (
<>

<main className="container my-12 mx-auto grid grid-cols-1 gap-2 md:gap-3


lg:gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4">

{
getPost?.map(
item => {
return <Card key={item.uuid} item={item} />
})
}
</main>

<PaginationItem item={getPost.meta.pagination} />

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 54/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

</>
)
}

Next.js SEO
If you are a blogger, you know how important SEO is in helping people find your blog and
your articles. For SEO, Next.js provides a generateMetadata function to generate dynamic
SEO metadata for your site. This means that you don't need any additional packages for
SEO.

For the purpose of this example, I'll explain how to enable SEO for the blog only on the
Homepage and the Reading page. You can use the same logic to enable it on any of your
other pages.

First, let's see how to enable SEO on the Homepage:

// ghost-client.ts

// Get you settings meta data from Ghost CMS


export async function getNavigation() {
return await api.settings.browse()
}

// src/app/page.tsx

import { getNavigation } from "./ghost-client"

export async function generateMetadata(): Promise<Metadata> {


const Metadata = await getNavigation()
return {
title: Metadata.title,
description: Metadata.description,
keywords: ['Next.js', 'React', 'JavaScript'],
}
}

Now we'll see how to enable SEO on the Reading page:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 55/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// ghost-client.ts

export async function getSinglePost(postSlug: string) {


return await api.posts
.read({
slug: postSlug
}, { include: ["tags", "authors"] })
.catch(err => {
console.error(err);
});
}

The generateMetadata have params props, which help access the slug. Then, based on the
slug, we get the data and return it.

export async function generateMetadata({ params }: { params: { slug: string }; }):


Promise<Metadata> {

const metaData: PostOrPage = await getSinglePost(params.slug)

let tags = metaData?.tags.map(item => item.name)

return {
title: metaData.title,
description: metaData.description,
keywords: tags,
openGraph: {
title: metaData.title,
description: metaData.excpet,
url: metaData.url,
keywords: tags,
images: [
{
url: metaData.feature_image,
},
],
locale: metaData.locale,
type: 'website',
},
}
}

How to Enable Search

Enabling search on a static blog is hard to do from scratch. Instead, you can use a third-
party Node page like Orama or Flex search.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 56/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

For our demo, we created a very simple search bar functionality without installing any
additional packages.

Firstly, we get all posts from the Ghost CMS API.

// ghost-client.ts

export async function getSearchPosts() {


return await api.posts.browse({ limit: "all"}).catch(err => {
console.log(err)
});

After we convert it into a string with the help of JSON.stringify(), we then create a new
search.json file. On every request, it updates or rewrites our search.json file.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 57/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

// src/app/page.tsx

import { getSearchPosts } from "./ghost-client"


import * as fs from 'node:fs';

export default async function Home() {

// get All posts for search


const AllPostForSerach = await getSearchPosts()

// Enable getSearch

try {

const jsonString = JSON.stringify(AllPostForSerach)

fs.writeFile('search.json', jsonString, 'utf8', err => {


if (err) {
console.log('Error writing file', err)
} else {
console.log('Successfully wrote file')
}
})

} catch (error) {
console.log('error : ', error)
}

return (
<>
<main className="container my-12 mx-auto grid grid-cols-1 gap-2 md:gap-3
lg:gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4">
{/* rest code... */}
</main>
</>
)
}

When you enter the text in the search input, based on the text query, we compare the query
or text in the serach.json file data. If it matches the article title with the query, then we store
the searchPost variable, and finally we render the stored data in the searchPost variable
page.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 58/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

"use client"

import React, { useEffect, useState } from 'react';


import * as Popover from '@radix-ui/react-popover';
import { FaSearch } from "react-icons/fa";
import Link from 'next/link';
import searchData from '../../search.json'
import type { PostOrPage } from "@tryghost/content-api"

let searchPost: PostOrPage[] = []

function Search() {

const [query, setQuery] = useState(null)

useEffect(() => {

searchPost.length = 0;

searchData.map((item: PostOrPage) => {

if (item?.title.trim().toLowerCase().includes(query?.trim().toLowerCase())) {
searchPost.push(item)
}

})

}, [query])

return (
<Popover.Root>
<Popover.Trigger asChild>
<button
className="cursor-pointer outline-none"
aria-label="Search"
>
<FaSearch />
</button>
</Popover.Trigger>

<Popover.Portal>

<Popover.Content
className="rounded p-2 bg-white dark:bg-gray-800 w-[480px] will-change-
[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-
[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-
[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-
slideRightAndFade"

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 59/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

sideOffset={5}
>

<div className='my-2'>
<label htmlFor="default-search" className="mb-2 mt-5 text-sm font-medium
text-gray-900 sr-only dark:text-white">Search bar </label>
<div className="relative">
<div className="absolute inset-y-0 left-0 flex items-center pl-3
pointer-events-none">
<svg className="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path
strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0
11-14 0 7 7 0 0114 0z"></path></svg>
</div>
<input type="search" id="default-search" onChange={(event) =>
setQuery(event?.target.value)} className="block w-full p-4 pl-10 text-sm text-gray-
900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-
blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-
white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Start
searching here ..." required />

</div>
</div>

serachPost.length > 0 ? serachPost.map(item => {

return (
<div key={item.uuid} className='my-3'>
<div className="text-white my-2 py-2 bg-blue-400 dark:bg-gray-900
dark:hover:bg-blue-400 border-none rounded-md dark:text-white">
<Link href={`read/${item.slug}`} className="relative inline-flex
items-center rounded-lg w-full px-4 py-2 text-sm font-medium">
{item.title}
</Link>
</div>
</div>
)
}) : " "

</Popover.Content>

</Popover.Portal>

</Popover.Root >
)
}

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 60/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

export default Search;

Error Handling
Next.js has two types of error handling. the first is layout-based, and the second is global
error handling. For the demo here, we'll use layout-based error handling.

Next provides a special type of error.tsx file to handle errors on your site. It does not
handle 404, 500, and so on – it handles only runtime errors.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 61/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

'use client'; // Error components must be Client components


import React from 'react';
import { useEffect } from 'react';
import Link from 'next/link';
export default function Error({ error, reset }: { error: Error; reset: () => void; })
{

useEffect(() => {
console.error(error);
}, [error]);

return (
<section className="dark:bg-gray-900 my-16">

<div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">

<div className="mx-auto max-w-screen-sm text-center">

<h1 className="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl


text-primary-600 dark:text-primary-500">Something wrong</h1>
<p className="mb-4 text-lg p-2 font-light bg-red-500 text-white dark:bg-
red-400 dark:text-white">{error.message}</p>

<div className='flex justify-around mt-2'>

<Link href="#" className="inline-flex bg-gray-600 text-white hover:bg-


gray-700 focus:ring-4 font-medium rounded-lg text-sm p-2
text-center">Back to Homepage</Link>

<button className='bg-gray-600 text-white rounded-lg p-2' onClick={() =>


reset()}>
Try again
</button>

</div>

</div>

</div>

</section>
);
}

How to handle 404 errors

To handle 404 errors in the Next.js app folder, you need to create a not-found.tsx file in
your root level.

Our 404 file looks like this:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 62/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

404 error

Here's the code for that:

import Link from "next/link"

function NotFound() {

return (
<section className="bg-white dark:bg-gray-900 my-16">
<div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
<div className="mx-auto max-w-screen-sm text-center">
<h1 className="mb-4 text-7xl tracking-tight lg:text-9xl text-primary-600
dark:text-primary-500">404</h1>
<p className="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-
4xl dark:text-white"> Something wrong</p>
<p className="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">
Sorry, we cant find that article. You will find lots to explore on the
home page.
</p>
<Link href="/" className="inline-flex text-white bg-black dark:bg-white
dark:text-black p-3 hover:bg-gray-800 my-4">Back to Homepage</Link>
</div>
</div>
</section >
)

export default NotFound

The big issue with the not-found.tsx error file is that it doesn't show automatically in Next
(v13.3.0). To show a 404 error, you need to show the error manually. Here's how you do that:

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 63/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

import { notFound } from 'next/navigation';

async function Read({ params }: { params: { slug: string }; }) {

const getPost = await getSinglePost(params.slug)

// if not found getPost, then show 404 error

if (!getPost) {
notFound()
}

return (
<main className="pt-8 pb-16 lg:pt-16 lg:pb-24 dark:bg-gray-900">

rest of code ....

</main>
)
}

How to Rebuild Your Static Site with Webhooks


The biggest problem when you create a static site happens if somebody writes a new post or
changes an existing post in Ghost. For a personal project, you can manually redeploy your
site. But for a larger site, you won't be able to do that every time this happens.

The best solution is to use webhooks. Ghost provides webhook support. If you update an
existing post or write a new one, it'll update in Ghost.

In the demo project, we're using Vercel webhooks to deploy our blog. When we create a new
blog or update something on the site, Ghost triggers the Vercel webhook. Then Vercel
rebuilds the site as needed.

You do not need to write the code for this – just follow along and copy-paste as you go.

How to get the webhook from Vercel

Firstly, go to the Vercel dashboard.

Vercel dashboard

Select your project, where you'll deploy your Ghost frontend.

Select the project in your Vercel dashboard

Click on the settings tab in your Vercel project.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 64/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Click the Git tab

Then click on the Git tab. After scrolling down, you can see the deploy hook selection.

Go to Deploy hooks sections

Enter your webhook name and branch name and click on the "create hook" button.

Copy your webhook url

Click on the copy button to copy your vercel webhook.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 65/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

How to integrate Vercel webhooks in the Ghost dashboard

When something changes in Ghost, it triggers the Vercel webhook URL. Then Vercel
redeploys the blog site.

To integrate the Vercel webhook with Ghost, just follow these steps:

Open the Ghost CMS dashboard.

Ghost dashboard

Click on the setting icon.

Ghost settings

Click on the New custom integration button.

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 66/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

Add a new custom integration

Enter the integration name.

Add integration name

Click to add the webhook button.

How to add the webhook

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 67/68
12/1/24, 12:28 PM How to Build a Blog with the Ghost API and Next.js

First, enter the name, then select Event and paste the URL which you copied from the Vercel
dashboard.

Based on the event, Ghost will call the webhook, and your website will rebuild. Redeploys
take time based on how big your site is, and so on.

Conclusion
Everything should work well using Next.js and the Ghost CMS as we've worked through in
this tutorial.

But some of the Ghost editor components, like toggles, where you need JavaScript
interaction, don't work. You can solve this by writing your own JavaScript or getting a
JavaScript file for Ghost and adding it to the read/[slug]/page.tsx file.

You can save a lot of money on hosting by combining Next.js and the Ghost CMS API, but
you lose some features like inbuilt signup, login, accounts, subscriptions, search bar, and
member access levels.

You can share and follow me on Twitter and Linkedin. If you like my work, you can read more
content on my blog, the officialrajdeepsingh.dev, frontend web, and Sign up for my free
newsletter.

You can also check out awesome-next, a curated list of awesome Nextjs-based libraries that
help build small and large-scale applications with Next.js.

Here are some additional resources you can use if you need more help or information while
going through this tutorial:

https://ghost.org/docs/jamstack/next/

https://www.digitalocean.com/community/tutorials/how-to-build-your-blog-on-digitalocean-
with-ghost-and-next-js

https://ghost.org/docs/content-api/

https://beta.nextjs.org/docs/getting-started
I write tons of articles on Next. If you are interested in Next and related stuff, you can follow
me on Medium and join the frontend web publication.

Rajdeep Singh

https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/ 68/68

You might also like