Download the template for this blog from Github

Next.js / Ghost Tutorial: 7 - Render blog posts

In the previous post, we established communication with the Ghost server using the Javascript API and added cards to the homepage to show a preview of the blog posts.

Now we are going to implement the [slug] pages that will get us to the full blog posts.

Enough of the preface, let's get work done!

[slugs]

Imagine how tedious it would be, having to create a route for every single blog post. But don't worry, dynamic routes are here to our rescue.

As already mentioned on Next.js / Ghost Tutorial: 5 - Routing and layout components, files named [...].js have special meaning in Next.js as they are used to create dynamic routes i.e. based on parameters given at runtime.


Ghost CMS generates a unique URL for every post (and page) you publish. Using [slug].js, we are able to retrieve them at runtime or, in the case of static generated sites (SSG) like the one we are building, at build time.

Render blog posts

To retrieve and render a blog post, we will need to:

  1. Create a function to fetch a single post through the Ghost API
  2. Determine all possible routing paths (getStaticPaths) and pass the content of a particular post as a prop object to the page component (getStaticProps).
  3. Render the post inside a component (PostPage)

Fetch a single post through the Ghost API

Add the following function to /api/ghost_data.js:

export async function getSinglePost(postSlug) {
 return await api.posts
  .read({
   slug: postSlug,
  })
 .catch((err) => {
  console.error(err)
 })
}

Note that we are passing the parameter postSlug corresponding to a post object

Determine routing paths, pass data as a property object and render post

Create the file /pages/posts/[slug].js with the following code:

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

import { getPosts, getSinglePost } from '../../api/ghost_data'
import Link from 'next/link'
import Layout from '../../components/layout'

// PostPage page component
export default function PostPage({ post }) {
  // Render post title and content in the page from props
  let _title = post.title + ' - My blog'
  return (
    <Layout _title={_title}>
      <div className="blogInnerHTML">
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
      <Link href="/Home" as={'/'}>
        <a>-- go to homepage --</a>
      </Link>
    </Layout>
  )
}

export async function getStaticPaths() {
  const posts = await getPosts()
  const paths = posts.map((post) => ({
    params: { slug: post.slug },
  }))
  return { paths, fallback: false }
}

// Pass the page slug over to the "getSinglePost" function
// In turn passing it to the posts.read() to query the Ghost Content API

export async function getStaticProps({ params }) {
  const post = await getSinglePost(params.slug)
  return { props: { post: post } }
}

Tip: Components (like PostPage above) start with a capital letter instead of being named with "Camel Case" notation.

Post styling

As you may have noticed, we use the class blogInnerHTML to style the HTML markup passed with the react attribute dangerouslySetInnerHTML. We take advantage of the hierarchical nature of css by setting blogInnerHTML as the scope for all tags contained within the div.

Tip: For consistent results, I strongly recommend that you compose your posts using Markdown. Check the section Further reading below for a basic Markdown syntax primer

Let's create the file /styles/blogInnerHtml.css with the following styling rules. Feel free to play around (and let us see the results 😉):

// File: /styles/blogInnerHtml.css

.blogInnerHTML {
  @apply w-11/12 py-8;
}
.blogInnerHTML h1 {
  @apply text-3xl text-gray-600 font-bold py-4;
}
.blogInnerHTML h2 {
  @apply text-2xl text-gray-600 font-bold py-4;
}
.blogInnerHTML h3 {
  @apply text-xl text-gray-600 font-bold py-4;
}
.blogInnerHTML p {
  @apply text-lg text-gray-700 py-px;
}
.blogInnerHTML ul,
ol {
  @apply my-4;
}
.blogInnerHTML ol li {
  @apply list-decimal ml-10;
}
.blogInnerHTML ul li {
  @apply ml-10;
  list-style-type: square;
}
pre {
  white-space: pre-wrap; /* Since CSS 2.1 */
  white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
  white-space: -pre-wrap; /* Opera 4-6 */
  white-space: -o-pre-wrap; /* Opera 7 */
  word-wrap: break-word; /* Internet Explorer 5.5+ */
}
.blogInnerHTML pre {
  @apply bg-gray-700 p-4 my-8 rounded-lg;
}
.blogInnerHTML code.language-javascript {
  @apply text-yellow-200 text-sm;
}
.blogInnerHTML a {
  @apply text-blue-700 font-light italic;
}
.blogInnerHTML img {
  @apply my-8;
}
.blogInnerHTML blockquote p {
  @apply bg-blue-100 text-blue-800 font-serif italic p-2 my-8 rounded-md;
}
.blogInnerHTML blockquote p::before {
  @apply text-2xl text-blue-600 font-extrabold font-serif mx-2;
  content: '\0022';
}
.blogInnerHTML blockquote p::after {
  @apply text-2xl text-blue-600 font-extrabold font-serif mx-2;
  content: '\0022';
}

For this styles to be globally available, we still need to import them into /pages/_app.js (remember?):

import '../styles/blogInnterHtml.css'

The whole file should now look like this:

import '../styles/tailwind.css'
import '../styles/globalstyles.css'
import '../styles/blogInnerHtml.css'
 
function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}
 
export default MyApp

You should now be able to display the whole posts.
Commit the changes to your code repository:

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

Celebrate the milestone!
Filipe Matos

Further reading