In this tutorial, we’ll create a static site with Next.js with blog data coming from Headless Ghost CMS.

The final code can be found here - https://github.com/albertkimdev/nextjs-headless-ghost-cms-blog

Prerequisites:

  • Intermediate Next.js and React.js knowledge.
  • Familiarity with blogs and CMS.

Technologies involved:

Steps involved:

  1. Set up a Ghost CMS application.
  2. Create a custom integration to get the API key to connect it with your Next.js frontend.
  3. Set up your Next.js app.
  4. Write functions using the Ghost Javascript SDK to fetch data from your Ghost blog.
  5. Use getStaticProps and getStaticPaths to generate static pages.
  6. Use getStaticProps to get all blog posts and link to them.
  7. Build dynamic blog posts with getStaticProps and getStaticPaths.

Set up a Ghost CMS application.

There are a few different ways to get a Ghost CMS application set up.

The CMS is just a Node.js application so all you need is a cloud server that can run a Node.js app.

This could be a Digital Ocean droplet or Google Cloud product or anything.

You could also use the official Ghost(Pro) hosting service.

In this tutorial, I’ll be covering how to create a Digital Ocean droplet that runs your Ghost CMS application. This is what I use as it’s simple, cost effective, and allows you to own everything.

Login to your Digital Ocean account and go to the Create Droplets page and select the Marketplace tab.

Search for “ghost” in the search input.

Select the “Ghost” “Basic” plan with “Regular Intel” and “$5/mo” if you want the cheapest plan.

Follow the rest of the steps and finish setting up your droplet. After that you should see a page initializing your droplet.

Login to the server via SSH or through the console:

If you login to the server via the console on Digital Ocean you’ll see a screen like:

The server is installing and setting up everything.

Then press enter and follow the steps to set up everything.

Enter your ip address if you haven’t set up your domain yet. You can update the domain later.

Once you’re done, you should see a screen like this:

Go to the website domain to finish setting up your Ghost blog. My domain to go to would look like http://167.172.77.38/ghost/

Follow the instructions on the page to finish setting up your Ghost CMS. The final page should look like:

Select “Explore Ghost admin” to go to the admin page

Now your Ghost CMS is set up!

Create a custom integration to get the API key to connect it with your Next.js frontend.

Click the settings button:

Click “Integrations”


Click “Add custom integration” at the bottom, enter a name and press “create”:

The two pieces of data you need are the Content API Key and API URL

To set your Ghost blog to private so people can’t access the website through the standard Ghost frontend and only allow access through your Next.js app, check the Make this site private option in the Settings -> General and then press Save.

Now you have everything you need to use the Ghost API and fetch blog data in your app!

Set up your Next.js app.

Now it’s time to set up your Next.js app.

I created a simple starter repo for a bare bones Next.js app that has Styled Components configured. You can find it here - https://github.com/albertkimdev/Nextjs-StyledComponents-Starter

The initial view when you run the starter should look like:

And your folder structure should look like:

pages/
  _app.js
  _document.js
  index.js

Now you have your Next.js project set up to become a statically generated blog with a Headless Ghost CMS.

Write functions using the Ghost Javascript SDK to fetch data from your Ghost blog.

Ghost provides a Javascript content API which makes it easy to get our blog data in JSON format.

Create a folder in your project called ghost and create a file called ghostData.js. This is where we’ll create our getters for the blog data.

Now your folder structure should look like:

ghost/
  ghostData.js
pages/
  _app.js
  _document.js
  index.js

Add the content api package:

yarn add @tryghost/content-api

Now in your file, add the code to initialize your Ghost API instance:

const GhostContentAPI = require("@tryghost/content-api");

const api = new GhostContentAPI({
  url: process.env.NEXT_PUBLIC_GHOST_URL,
  key: process.env.NEXT_PUBLIC_GHOST_KEY,
  version: "v3",
});

We’re using environment variables to intialize the GhostContentAPI instance so our data is secured and not anyone can read it if you push your code to a repository.

To use environment variables in a Next.js app, just create a .env.local file in your root directory and add your environment variables like:

NEXT_PUBLIC_GHOST_URL=http://google.com
NEXT_PUBLIC_GHOST_KEY=1233545345345

And in your app you’ll be able to access your variables.

Don’t forget to add your .env.local file to your .gitignore file.

Next, we’ll create and export functions that use the API to get blog data and return it.

Add this code to your file:

export async function getPosts() {
  return api.posts
    .browse({
      limit: "all",
      include: "tags,authors",
    })
    .catch((err) => {
      console.error(err);
    });
}

export async function getSinglePost(postSlug) {
  return api.posts
    .read({
      slug: postSlug,
      include: "tags,authors",
    })
    .catch((err) => {
      console.error(err);
    });
}

The first function, getPosts, gets all the posts and includes the tags and authors metadata.

If you run the code, the output would look like:

As you can see, it’s just an array of all your posts on Ghost in JSON format.

We use getPosts to find all the blog posts so we can display them in a list. Then we’ll create cards for each post so users can click them to read the selected post.

We also fetch tags to display the tags and be able to implement a search filter using tags. Also the authors to show the authors of the post.

getSinglePost takes a slug parameter and returns the data for a single post. This is the function that we use to build the actual blog post.

If you run the code with a slug, the output looks like:

It’s just a JSON object with the data of the Ghost blog post.

We use the html key to insert the generated HTML code inside our React app.

Now we have the functions we need to create static pages.

Use getStaticProps and getStaticPaths to generate static pages.

Next.js is a React app which means it can use client side rendering to show the website.

We don’t want that. Client side rendered websites are good for web apps with dynamic data that change and update.

Since we have basic data that’s ready, like a blog post, we can tell Next.js to generate the pages ahead of time and serve static sites to our users, using data that’s pre-fetched.

This is good for SEO and performance reasons.

To build static pages with pre-fetched data in Next.js, we use getStaticPaths and getStaticProps.

getStaticPaths tells Next.js which pages should be pre-built. So if you have a list of blog posts titled, “How to build a website”, “how to use google”, “how to use yahoo,” Next.js will build those pages so they’re available statically.

getStaticProps tells Next.js what data each page should have. It passes props to the component so you can build your site with data from props and not fetch it after the component is loaded. This is useful for SEO because your website has relevant content and not something like “loading…”

Using the Ghost API functions and the Next.js build functions, we can start building out the website with data.

In index.js, we’ll get all the blog posts and create links to those posts.

The posts will be in a page called /blog/[slug].js.

The [slug] is the identifier to fetch specific blog post data. This will all make sense later.

In index.js, add the code:

export const getStaticProps = async () => {
  let posts = await getPosts();

  return {
    props: {
      posts,
    },
  };
};

By adding this method to the index.js file, we tell Next.js:

  • This page should be statically generated.
  • Use the Ghost function to fetch all posts and then pass that data to the component.

Now in the component we can access the posts from the props.

const Index = ({ posts }) => {
  console.log(posts);
  return (
    <Wrapper>
      <p>Hello world</p>
    </Wrapper>
  );
};

And in the browser I can see the logged posts:

Now I can map over the posts and create a link to the blog post:

const Index = ({ posts }) => {
  return (
    <Wrapper>
      <h1>Hello world</h1>
      {posts.map((post) => (
        <a target='_blank' href={`blog/${post.slug}`} key={post.id}>
          {post.title}
        </a>
      ))}
    </Wrapper>
  );
};

Now my website looks like this:

The links would look like: http://localhost:3000/blog/coming-soon

Now we’ll build out the blog/[slug.js] part.

Build dynamic blog posts with getStaticProps and getStaticPaths.

Create a new folder and file like blog/[slug].js

Your folder structure should look like:

ghost/
  ghostData.js
pages/
  blog/
    [slug].js
  index.js

This is the page that will be the blog post.

If your blog post url is http://website.com/blog/blog-post-title the slug would be blog-post-title.

Then Next.js uses that slug as the parameter in getSinglePost to find the specific post data.

This file needs to use both getStaticProps and getStaticPaths.

getStaticPaths will tell Next.js how many unique pages there are. So if your blog has 5 blog posts then Next.js will build 5 pages using the unique slug.

getStaticProps returns the blog post data to the component to build it with props and you don’t have to fetch it after the component loads.

Your Post.js file should now look like:

const Post = ({ post }) => {
  console.log(post);
  return <div>Post</div>;
};

export const getStaticPaths = async () => {
  const posts = await getPosts();

  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }));
  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps = async ({ params: { slug } }) => {
  const post = await getSinglePost(slug);

  return {
    props: {
      post,
    },
  };
};

In getStaticPaths, we’re getting all the posts and turning it into an array of objects with a params key which is an object containing the slug.

Then this tells the Next.js build process to build out all the pages in the paths array.

The slug key should match whatever is in the [] brackets of your page. So if your dynamic page is blog/[id].js then the object should use id as the key inside params and not slug.

The fallback option tells Next.js to not pre-build your page but to build it for the first time when someone visits your page. This is useful for blog sites with many posts or ecommerce sites with a lot of pages so you don’t spend a lot of time waiting for your site to build.

getStaticProps works together with getStaticPaths if they’re used in the same file. getStaticProps takes in the slug params and using that, you can find the single post using the Ghost method, and then return that data as props.

If I click the link to the Coming soon post in index.js or go to http://localhost:3000/blog/coming-soon, I’ll be able to see the logged post data.

The object we want to use is the html key which contains the blog data in HTML format.

Use this code to insert it into your component React style:

const Post = ({ post }) => {
  return (
    <div
      dangerouslySetInnerHTML={{
        __html: post.html,
      }}
    />
  );
};

The dangerouslySetInnerHTML is how React renders HTML code from a string into your div.

Now my page looks like:

And in the inspector the code looks like:

This is very basic but it works!

To style your page you can create a CSS wrapper and target the elements in your wrapper like p tags or h1 tags or whatever other elements are generated from your Ghost post.

Now you have a functional Next.js statically generated website using Headless Ghost CMS!