Go back

Adding post controllers to my blog

My intention from the get-go was to create two buttons that'll let you navigate to the next or previous article from this article that you're reading now.

I was ecstatic as I had already established a miniature pattern and formulated the steps I'll embark on with the flowchart in my head. "How hard could it be?". And for the first time — Yes, how hard could it be, really? Little did I know that I'll encounter some obstacles as I progress. I'll save that for later.

One goal I had in mind while building this was to make it as simple as possible. No need to "over-engineer" and ruin it all.

A good side-effect

Realizing the cause of this error, relieved me.

My intention again, was to let you go directly to the next article on my blog, I realized that after adding this tiny detail, the logic behind how the articles on my blog are rendered made the buttons more dynamic.

Normally, when you scroll to the bottom of this article, you should see two UI elements serving as buttons, pointing to the next article or a previous one. But, that's not the case here;

Since I am mutating the array of articles on my blog by rendering a featured article — normally again, the featured article on most blogs are the latest ones.

But here, it is different, the featured one is a random article from the array of articles on the blog.

const randomFeatured = Math.floor(Math.random() * posts.length)
let featuredPost = posts[randomFeatured]

When the featured post is identified, it is removed from the posts array by filtering its id property like so:

const updatedPosts = posts.filter((object) => object.id !== featuredPost.id)

This alone alters the order of the articles in the array. Hence the post controllers that let you navigate to another article are generated randomly.

Establishing a mental model

Realizing the cause of this error as I said previously, relieved me. At least, now, I know where to go from there.id previously, "How hard could this be?".

I figured out that Since I have a function that returns all the articles for me, I can get the index of the current article, increment its index when I click on the next button, and decrement it when I want to read the previous article and that's all.

// /blog/[slug].js
 
import getAllArticles from '@utils/mdx'
 
const articles = (await getAllArticles()).map((article) => article)
 
console.log(articles) // returns all the articles as an array of objects

But then, I ran into a build issue in production. Locally with localhost:3000 everything worked fine — another wake-up call to always run the build script before pushing to prod. Here's the error below;

prerendering error on caleb's blog via netlify

If you take a look at the image above, you'll see that there's a prerendering error for the "/blog/year-in-review-2022" and "/blog/building-a-nextjs-preloader-the-right-way" articles. This particular issue got me wondering for several hours, what the cause could be.

After yelling endlessly at my PC and asking JavaScript kept tormenting me, I found out that the reason why the title properties on both articles were undefined is that getAllArticles() — the helper function I mentioned previously — returns all the mdx files in alphabetical order, like so:

.
└── articles/
    ├── building-a-nextjs-preloader-the-right-way.mdx
    ├── prevent-default-scroll-event.mdx
    ├── reusable-youtube-component.mdx
    ├── styled-components-hydration-error.mdx
    ├── styling-nextjs-image-component.mdx
    ├── svg-animation-with-remotion.mdx
    ├── the-css-overview-devtool.mdx
    ├── times-up.mdx
    ├── type-checking-in-typescript.mdx
    ├── useref-not-queryselector.mdx
    ├── using-proptypes-in-react.mdx
    └── year-in-review-2022.mdx

And in a reversed manner, the last item in the articles folder — year-in-review-2022.mdx — receives the first index value, zero (0).

So, when you go to either of the links, say, the first one for example, and you click on the prev button, you're performing this operation 0 - 1, which in turn becomes -1.

There's no article in the array with a negative index. Hence the reason I got the "cannot read property of undefined (reading title)" error.

The same thing happens with the last item in the array. Incrementing its value will result in a new index that does not exist.

Finding a workaround

The first step I took was to look for a way to sort the list of articles based on the date they were published so that the order is proper.

const articles = (await getAllArticles()).map((article) => article)
articles.sort((a, b) => {
  return new Date(b.publishedAt) - new Date(a.publishedAt)
})
const articleSlugs = articles.map((articleSlug) => articleSlug.slug)

I went on to assign the slug variable to a new one — currentPost, got the index of the last article by subtracting 1 from its length, and found the index of the current article with the indexOf() method.

currentPost = slug
lastIndex = articles.length - 1
currentIndex = articleSlugs.indexOf(currentPost)

These modifications, however, did not fix the error though. I still had to look for a way to conditionally render the values assigned to the nextIndex and prevIndex variables that I already created.

nextIndex = currentIndex === lastIndex ? currentIndex : currentIndex + 1
prevIndex = currentIndex === 0 ? currentIndex : currentIndex - 1

Remember how I explained the arithmetic process behind the last and first index issues? I realized that, to calculate the nextIndex I need to check if the currentIndex is the lastIndex. If it is I return the current index as a value, if not, I increment the value.

For the prevIndex the logic remains the same. But, this time, I'm comparing the currentIndex with 0 and I decrement if it does not meet the condition.

The next thing on my list was to get the title and slug of each article respectively by assigning them to the values of the indexes I already obtained.

const postsTitle = (await getAllArticles()).map((post) => post.title)
const postSlugs = (await getAllArticles()).map((post) => post.slug)
 
nextPostTitle = postsTitle[nextIndex]
prevPostTitle = postsTitle[prevIndex]
nextPostSlug = postSlugs[nextIndex]
prevPostSlug = postSlugs[prevIndex]

You may have wondered why there have been little to no variable declarations in the snippets I've been sharing. Well, you can have a look at them now in this getStaticProps() method.

export async function getStaticProps({ params }) {
  let nextIndex
  let prevIndex
  let lastIndex
  let currentPost
  let currentIndex
  let nextPostSlug
  let prevPostSlug
  let nextPostTitle
  let prevPostTitle
 
  //fetch the particular file based on the slug
  let { slug } = params
  const { content, frontmatter } = await getArticleFromSlug(slug)
  const mdxContent = extractHeadings(`./data/articles/${slug}.mdx`)
  const articles = (await getAllArticles()).map((article) => article)
 
  articles.sort((a, b) => {
    return new Date(b.publishedAt) - new Date(a.publishedAt)
  })
 
  const articleSlugs = articles.map((articleSlug) => articleSlug.slug)
 
  currentPost = slug
  lastIndex = articles.length - 1
  currentIndex = articleSlugs.indexOf(currentPost)
 
  nextIndex = currentIndex === lastIndex ? currentIndex : currentIndex + 1
  prevIndex = currentIndex === 0 ? currentIndex : currentIndex - 1
 
  const postsTitle = (await getAllArticles()).map((post) => post.title)
  const postSlugs = (await getAllArticles()).map((post) => post.slug)
 
  nextPostTitle = postsTitle[nextIndex]
  prevPostTitle = postsTitle[prevIndex]
  nextPostSlug = postSlugs[nextIndex]
  prevPostSlug = postSlugs[prevIndex]
 
  return {
    props: {
      post: {
        frontmatter,
        fileContent: mdxContent,
        controllers: {
          nextPostSlug,
          nextPostTitle,
          prevPostSlug,
          prevPostTitle,
        },
      },
    },
  }
}

The snippet above is what my approach looked like. I'm using the let keyword, so I can re-assign these variables along the way and be able to pass them as props on the page.

Rendering a UI

Now that I have the slug and title variables in the controllers object, I can use them in the current article's UI like so:

// blog/[slug].js
 
import Head from "next/head"
import Link from "next/link"
 
export default function Blog({
  post: {
    frontmatter: {
      title,
    }
    controllers: { nextPostSlug, prevPostSlug, nextPostTitle, prevPostTitle },
  },
}) {
  return (
    <React.Fragment>
      <Head>
        <title>{title} | Caleb&apos;s blog</title>
      </Head>
      <Layout>
        <BlogPostWrapper>
          <div className="content">
            <article>// article's content goes here</article>
          </div>
          <div className="post-controllers">
            <div className="controllers-block">
              <Link href={`/blog/${prevPostSlug}`}>
                <div className="controller prev__controller">
                  <span>
                    <BsArrowLeft />
                  </span>
                  <p>{prevPostTitle}</p>
                </div>
              </Link>
              <Link href={`/blog/${nextPostSlug}`}>
                <div className="controller next__controller">
                  <p> {nextPostTitle}</p>
                  <span>
                    <BsArrowRight />
                  </span>
                </div>
              </Link>
            </div>
          </div>
        </BlogPostWrapper>
      </Layout>
    </React.Fragment>
  )
}

And that's it — again.

Let me know if you have any questions by tweeting at me here: @kafLamed