Download the template for this blog from Github

Next.js / Ghost Tutorial: 6 - Speaking with Ghost(s)

After getting the development environment up and running and creating our Next.js project from scratch, we configured it for Tailwind CSS. In the last episode we coded the layout components and talked about routing with Next.js.

Now, we will interact with the Ghost CMS backend.

To connect with your CMS server, you need a API key. Open your Ghost admin site and follow this steps to generate it:

  1. On the Settings section, choose Integrations
    generate_API_Key_1

  2. Select +Add custom integration
    generate_API_Key_2

  3. Give your new integration a name of your preference
    generate_API_Key_3

  4. Copy the values Content API Key and API URL for later and save
    generate_API_key_4

Tip: From now on, for things to make sense, make sure to have at least two sample blog posts as described on episode 2 - Section Generate sample data

Connect to Ghost API

Create a new file named ghost_data.js under the /api folder. This file will be used to store the API connection data as well as the functions needed to retrieve data from the Ghost server.

Enter the code below and replace the placeholders <api url> and <content key> (remove the angle brackets too) with your own values:

// file: /api/ghost_data.js

import GhostContentAPI from '@tryghost/content-api'

// Create API instance with site credentials
const api = new GhostContentAPI({
  url: '<api url>',
  key: '<content key>',
  version: 'v3',
})

With the new constant api, retrieve all posts from Ghost by adding the following function right below:

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

Once called, getPosts() will fetch all (published) posts along with some extra data (include: 'tags,authors').
If something goes wrong, catch will log it to the console.

Display post in preview cards

We want to show all blog posts on the homepage in individual "preview cards". Therefore, we switch to /pages/index.js and import getPosts() with the following line of code:

import { getPosts } from '../api/ghost_data'

Now, we add the function getStaticProps(). This special Next.js methode retrieves the data for static pages beforehand (the index page, in our case).

export async function getStaticProps() {
	const posts = await getPosts()
	
	posts.map((post) => {
		const options = {
		year: 'numeric',
		month: 'short',
		day: 'numeric',
	}

	post.dateFormatted = new Intl.DateTimeFormat('default', options).format(
	new Date(post.published_at),
	)
	})
	return {
		props: {
		posts,
		},
	}
}

We use Intl.DateTimeFormat() to convert the publishing date to a human readable format.

Next, get rid of the existing Home function and rewrite it as follows:

export default function Home({ posts }) {
  return (
    <Layout home _title="My Ghost Blog">
      <ul>
        {posts.map((post) => (
          <li>
            <PostPreviewCard blogpost={post} />
          </li>
        ))}
      </ul>
    </Layout>
  )
}

The rewriten component iterates through posts (which has been passed with the props object) and display them using the PostPreviewCard component. Since we have not written this component yet, running this code would throw an error.

Before leaving index.js, import both components Layout and PostPreviewCard:

import Layout from '../components/layout'
import PostPreviewCard from '../components/postpreviewcard'

The whole /pages/index.js should now look like this:

// File: /pages/index.js

import PostPreviewCard from '../components/postpreviewcard'
import styles from '../styles/Home.module.css'
import { getPosts } from '../api/ghost_data'
Import Layout from '../components/layout'

export default function Home({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.key}>
          <PostPreviewCard blogpost={post} />
        </li>
      ))}
    </ul>
  )
}

export async function getStaticProps() {
  const posts = await getPosts()

  posts.map((post) => {
    const options = {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
    }

    post.dateFormatted = new Intl.DateTimeFormat('default', options).format(
      new Date(post.published_at),
    )
  })
  return {
    props: {
      posts,
    },
  }
}

The PostPreviewCard component

Create the file postpreviewcard.js under the folder /components and edit it to look like this:

// file: /components/postpreviewcard.js

import Link from 'next/link'
 
export default function BlogPreviewCard({ blogpost }) {
  return (
    <div className="grid my-12">
      <div className="grid place-self-center w-3/4 grid-cols-1 md:grid-cols-2 mb-8 border-b-2 border-gray-100 border-dotted ">
        <div className="flex justify-start md:justify-center items-center">
          <Link href="/posts/[slug]" as={`/posts/${blogpost.slug}`}>
            <img className="m-8 rounded-l h-32 " src={blogpost.feature_image} />
          </Link>
        </div>
        <div className="flex md:pt-4">
          <div className="ml-5">
            <p className="text-gray-500 text-sm">
              {blogpost.tags.map((tag) => (
                <span className="mr-3">
                  <Link href="/tags/[slug]" as={`/tags/${tag.slug}`}>
                    <a>{tag.name}</a>
                  </Link>
                </span>
              ))}
            </p>
            <p>{blogpost.dateFormatted}</p>
            <hr />
            <Link href="/posts/[slug]" as={`/posts/${blogpost.slug}`}>
              <a>
                <h1 className="text-2xl font-bold text-indigo-900">
                  {blogpost.title}
                </h1>
              </a>
            </Link>
            <div
              className="excerpt py-2"
              dangerouslySetInnerHTML={{ __html: blogpost.excerpt }}
            />
            <Link href="/posts/[slug]" as={`/posts/${blogpost.slug}`}>
              <a className="text-indigo-900">...read more</a>
            </Link>
          </div>
        </div>
      </div>
    </div>
  )
}

Wow, there's a lot going on here! Let's go through it:

  1. Import the next/link component
  2. Declare the function PostPreviewCard and take a post object as a parameter
  3. Display the following blog post data:
    • feature_image
    • tags (we iterate through an array of tags in case the post has been assigned multiple of them)
    • dateFormatted (remember it?)
    • title
    • excerpt

In several places in the code, we set links to /posts/[slug], the unique url for the blog post. We'll implement the [slug] pages in the next episode, until then the links are not functional.


Commit the changes to your code repository.

git add .
git commit -m "Added Tailwind css"
git push

Stay tuned for more!
Filipe