Download the template for this blog from Github

Next.js / Ghost Tutorial: 8 - Render blog pages and tags

After having seen in the previous post how to render blog posts, we will now use a very similar logic to render our blog pages and tags. Furthermore we will build a dynamic page with an overview of all existing tags and their respective posts.

Before continuing, make sure you have already created the sample pages as described on 2 - Setting up the development environment.

Fetch blog pages

I must admit that I couldn't, at first, tell the difference between a blog post and a blog page. Both look pretty much the same and even the Ghost backend GUI looks (almost) the same for both.
There are however some small yet important differences, being the most relevant that pages are made for static content whereas posts have more of a "episode" character, a part of a series, so to say.
You can read more about this directly from the official Ghost documentation:
https://ghost.org/docs/publishing/#pages

As you will soon discover, the similarities are also noticeable on the Ghost Content API, since the method to fetch pages is pretty much identical with the one we already used to fetch posts.

With that in mind, let us add two new functions to /api/ghost_data.js: one will be needed to retrieve all blog pages at once and the other to retrieve a single page.

// retrieve all pages

export async function getPages() {
  return await api.pages
    .browse({
      limit: 'all',
    })
    .catch((err) => {
      console.error(err)
    })
}

// retrieve one single page

export async function getSinglePage(pageSlug) {
  return await api.pages
    .read({
      slug: pageSlug,
    })
    .catch((err) => {
      console.error(err)
    })
}

Render blog pages

Next, create the file /pages/blogpages/[slug].js with the following code:

// File: /pages/blogpages/[slug].js

import { getPages, getSinglePage } from '../../api/ghost_data'
import Link from 'next/link'
import Layout from '../../components/layout'
 
export default function PostPage({ page }) {
  // Render post title and content in the page from props
  let _title = page.title + ' - My blog'
  return (
    <Layout _title={_title}>
      <div className="blogInnerHTML">
        <h1>{page.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: page.html }} />
      </div>
    </Layout>
  )
}
 
export async function getStaticPaths() {
  const pages = await getPages()
  const paths = pages.map((page) => ({
    params: { slug: page.slug },
  }))
  return { paths, fallback: false }
}
 
export async function getStaticProps({ params }) {
  const page = await getSinglePage(params.slug)
  return { props: { page: page } }
}

Since the code is similar to the previous post, I'll skip detailed explanations on this one.


You should now be able to use the links on the navigation bar and the copyright link on the footer to fetch and render the respective blog pages.
That's it!

Fetch tags

It is now time to retrieve the tags with the help of the content API.
Add the following three functions to /api/ghost_data.js:

// retrieve all tags

export async function getTags() {
  return await api.tags
    .browse({
      limit: 'all',
    })
    .catch((err) => {
      console.error(err)
    })
}
 
// retrieve a single tag

export async function getSingleTag(tagSlug) {
  return await api.tags
    .read({
      slug: tagSlug,
    })
    .catch((err) => {
      console.error(err)
    })
}

// retrieve all posts associated with a particular tag

export async function getPostsByTag(tag) {
  const posts = await api.posts
    .browse({
      filter: `tag:${tag}`,
    })
    .catch((err) => {
      console.error(err)
    })
  return posts
}

getTags() fetches all tags (limit: 'all')
getSingleTag(ID) takes a tag ID as a parameter to retrieve it from Ghost
getPostsByTag(ID) retrieves all posts containing the tag ID

Render tags with associated posts

Whenever a visitor clicks a tag name, for example on a preview card, we want to display a list of posts which have also been tagged with that particular keyword.
To achieve this, create the file /pages/tags/[slug].js with the following content:

// File: /pages/tags/[slug].js

import { getTags, getSingleTag, getPostsByTag } from '../../api/ghost_data'
import Link from 'next/link'
import Layout from '../../components/layout'

export default function TagPage(tagData) {
  let _title = tagData.tag.name + ' - My blog'
  return (
    <Layout _title={_title}>
      <div className="my-10">
        <h2 className="py-1 mx-2 my-8  text-indigo-900 text-xl">
          Posts tagged with{' '}
          <span className="font-bold">{tagData.tag.name}</span>
        </h2>
        <ul>
          {tagData.posts.map((post) => (
            <li>
              <div className="cursor-pointer py-4 bg-gray-100 hover:bg-gray-200 m-2 rounded-md border-gray-200 border-2">
                <Link href="/posts/[slug]" as={`/posts/${post.slug}`}>
                  <div className="mx-10">
                    <h3 className="font-medium text-indigo-900">
                      {post.title}
                    </h3>
                    <p className="text-gray-600 text-xs">
                      {new Intl.DateTimeFormat('default', {
                        year: 'numeric',
                        month: 'short',
                        day: 'numeric',
                      }).format(new Date(post.published_at))}
                    </p>
                    <p className="italic text-gray-700 text-xs">
                      (reading time: {post.reading_time} min.)
                    </p>
                  </div>
                </Link>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </Layout>
  )
}

export async function getStaticPaths() {
  const tags = await getTags()
  const paths = tags.map((tag) => ({
    params: { slug: tag.slug },
  }))
  return { paths, fallback: false }
}

// Pass the tag slug over to the "getSingleTag" function
// and retrieve all associated posts

export async function getStaticProps({ params }) {
  const _tag = await getSingleTag(params.slug)
  let _posts = (await getPostsByTag(params.slug)).sort((a, b) => {
    return a.published_at > b.published_at ? -1 : 1
  })
  return { props: { tag: _tag, posts: _posts } }
}

The component TagPage accepts an object with the format {tag: _tag, posts: _posts}, calls the Layout component and iterates through the posts array with tagData.posts.map((post) => ... to display the details.

The second function getStaticPaths() collects all tags.

Finally, the function getStaticProps() composes the props object _tag with the tag information and the _posts array to be passed to the component for further processing.

Tag overview

We will now build a page with an overview of all published tags as well as a counter of the associated posts. You may want to use it as a starting point to build a tag cloud or any other visualisation form.

Since we already have all required API call functions in place, we don't need to touch the API file this time.
Instead, create a file named /pages/tags/tagoverview.js and put the following code in it:

// File: /pages/tags/tagoverview.js

import { getTags, getSingleTag, getPostsByTag } from '../../api/ghost_data'
import Layout from '../../components/layout'
import Link from 'next/link'

export default function TagOverview({ tagObjects }) {
  return (
    <Layout _title="Tag Overview - My Blog">
      <div className="my-10">
        <h2 className="py-1 mx-2 my-8 text-indigo-900 text-2xl font-bold">
          Tag Overview
        </h2>
        {tagObjects.map((tag) => (
          <div className="mb-10">
            <h2 className="py-1 mx-2 my-8 text-indigo-900 text-xl">
              {tag.posts.length} post{tag.posts.length > 1 ? 's' : ''} tagged
              with <span className="font-bold">{tag.tag.name}</span>
            </h2>
            <ul>
              {tag.posts.map((post) => (
                <li className="cursor-pointer py-4 bg-gray-100 hover:bg-gray-200 m-2 rounded-md border-gray-200 border-2">
                  <Link href="/posts/[slug]" as={`/posts/${post.slug}`}>
                    <div className="mx-10">
                      <h3 className="font-medium text-indigo-900">
                        {post.title}
                      </h3>
                      <p className="text-gray-600 text-xs">
                        {new Intl.DateTimeFormat('default', {
                          year: 'numeric',
                          month: 'short',
                          day: 'numeric',
                        }).format(new Date(post.published_at))}
                      </p>
                    </div>
                  </Link>
                </li>
              ))}
            </ul>
          </div>
        ))}
      </div>
    </Layout>
  )
}

export async function getStaticProps() {
  let tagObjects = []

  for (const _tag of await getTags()) {
    let _posts = (await getPostsByTag(_tag.slug)).sort((a, b) => {
      return a.published_at > b.published_at ? -1 : 1
      return 0
    })

    tagObjects.push({
      tag: _tag,
      posts: _posts,
    })
  }

  return {
    props: { tagObjects },
  }
}

The tagObjects object we are building up inside getStaticProps() has the format:
{
{{tag_1}, {{post_1}, {post_2}, {post_x}}
{{tag_2}, {{post_1}, {post_2}, {post_x}}
{{tag_x}, {{post_1}, {post_2}, {post_x}}
}

We then wrap in the the props object and pass it as an argument to the component tagOverview so we than can iterate through the tags and respective posts and display them.

You definitely want to push this version to your repository:

git add .
git commit -m "Added functionality to fetch and render pages"
git push

From the styling point of view there are a lot of edges to be polished but this is something I encourage you to do on your own, branding it according to your taste.
Feel free to use this project as a starting point for your own blog, customizing and extending the features as needed.

Next time we will learn how to build and export the static page so it can be deployed to your web server or preferred hosting provider.

Stay tuned!

Filipe Matos