Create a Robust Blog-Post-Reads Counter in Next.js

Amarachi Iheanacho - May 3 '22 - - Dev Community

Blog reads tells us how often our content has been read. They help us understand how much traffic our content has been getting and we can use this data to strategize and repurpose our content accordingly.

What we will be building

This post will discuss using the react-intersection-observer library to implement a blog read counter that is incremented when a user has read 70 percent of our blog post. These blog reads are stored on a database. You don’t need a custom server to manage this.

GitHub URL

https://github.com/Iheanacho-ai/appwrite-blog-read-counter

This is a Next.js project bootstrapped with create-next-app.

Getting Started

First, run the development server:

npm run dev
# or
yarn dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 with your browser to see the result.

You can start editing the page by modifying pages/index.js. The page auto-updates as you edit the file.

API routes can be accessed on http://localhost:3000/api/hello. This endpoint can be edited in pages/api/hello.js.

The pages/api directory is mapped to /api/*. Files in this directory are treated as API routes instead of React pages.

Learn More

To learn more about Next.js, take a look at the following resources:

You can check out the Next.js GitHub repository - your feedback and contributions are welcome!

Deploy on Vercel

The easiest way to deploy your Next.js app is to use the Vercel Platform from…

Prerequisites

To get the most out of this project, we require the following:

  • A basic understanding of CSS, JavaScript, and React.jsDocker Desktop is installed on the computer, run the docker -v command to verify if we have docker desktop installed, if not, install it from here
  • An Appwrite instance running on our computer; check out this article to create a local Appwrite instance. We will use Appwrite’s powerful database service and experience to manage our analytics data.

Setting up our Next.js app

Next.js is an open-source React framework that enables us to build server-side rendered static web applications.

To create our Next.js app, we navigate to our preferred directory and run the terminal command below:

    npx create-next-app@latest
    # or
    yarn create next-app
Enter fullscreen mode Exit fullscreen mode

After creating our app, we change the directory to the project and start a development server with:

    cd <name of our project>
    npm run dev
Enter fullscreen mode Exit fullscreen mode

To see our app, we go to http://localhost:3000.

Installing react-intersection-observer

React-intersection-observer is a react library that allows developers to monitor when an element enters or leaves the viewport.

To install the react-intersection-observer in our application, run this terminal code.

    npm install react-intersection-observer --save

    #or

    yarn add react-intersection-observer
Enter fullscreen mode Exit fullscreen mode

Installing Appwrite

Appwrite is an open-source, end-to-end, back-end server solution that allows developers to build applications faster.

To use Appwrite in our Next.js application, we install the Appwrite client-side SDK for web applications.

    npm install appwrite
Enter fullscreen mode Exit fullscreen mode

Creating a new Appwrite project

During the creation of the Appwrite instance, we specified what hostname and port we see our console. The default value is localhost:80.

We go to localhost:80 and create a new account to see our console.

On our console, there is a Create Project ****button. Click on it to start a new project.

Appwrite

Our project dashboard appears once we have created the project. At the top of the page, there is a Settings bar. Click it to access the Project ID and API Endpoint.

Appwrite

We copy the Project ID and API Endpoint, which we need to initialize the Appwrite Web SDK.

Appwrite

In our project's root directory, we create a utils.js file to initialize our Web SDK.

    import { Appwrite } from 'appwrite';
    // Init your Web SDK

    export const sdk = new Appwrite();
    sdk
    .setEndpoint('http://localhost/v1') // Your API Endpoint
    .setProject('625d9071b3fc17c7bfb6') // Your project ID
    ;
Enter fullscreen mode Exit fullscreen mode

Creating an anonymous user session

Appwrite requires a user to sign in before reading or writing to a database to enable safety in our application. However, they allow us to create an anonymous session for small projects like this.

We use a function to create an anonymous user session once this application mounts. We will run this createAnoymousSession function later in this tutorial.

    import { Appwrite } from 'appwrite';
    // Init your Web SDK

    export const sdk = new Appwrite();
    sdk
    .setEndpoint('http://localhost/v1') // Your API Endpoint
    .setProject('625d9071b3fc17c7bfb6') // Your project ID
    ;

    //Creating anonymous Session
    export const createAnonymousSession = async() => {
        try{
            await sdk.account.createAnonymousSession();
        }catch(err){
            console.log(err)
        }    
    }
Enter fullscreen mode Exit fullscreen mode

Creating the collection and attributes

In the Appwrite web Console, we click on Database on the left side of the dashboard.

Appwrite

We create a collection in our database tab by clicking on the Add Collection button. This action redirects us to a Permissions page.

At the Collection Level, we want to assign a Read Access and Write Access with a role:all ****value. You can modify these permissions to specify who has access to read or write to our database.

Appwrite

Next, we go to our Attributes tab to create the properties we want a document to have.

Appwrite

We create an integer attribute of blogCount.

Appwrite

Creating an Appwrite Document

After creating the attributes our document will have, we go to the Documents section of the Database.

Appwrite

Next, we click on the Add Document button and input a zero value to create a document in our collection. This value represents the blog reads.

Appwrite

On the right of our newly created document, we copy our Collection ID and Document ID, which we need to perform functions on this document.

Appwrite

Creating the Blog Post.

In our index.jsx file, we create a sample blog post that we will use to measure and store our blog reads.

https://gist.github.com/Iheanacho-ai/6852e1cd43e37fe457a45ae702a109ba

Next, we elevate our blog post user interface by adding styles in the global.css file.

https://gist.github.com/Iheanacho-ai/c35dc718ccbd62cc0339a4a18e5ee3b2

Here is how our blog post looks.

Appwrite

Utilizing React-intersection-observer to count blog reads

The react-intersection-observer library allows us to run a function or perform a task when a part of the website is visible in the viewport. We want to increase our blog count reads when a user has read 70 percent of the blog post with this logic.

    import {useEffect, useState} from 'react';
    import { useInView } from 'react-intersection-observer';

    const Home = () => {
      const { ref: myRef, inView } = useInView();
      const [appBlogRead, setAppBlogRead] = useState(0)
      const [blogCount, setBlogCount] = useState()

      return (
          <div className="blog">
            {/* The rest of the app goes here */}
              ...
          </div>
        )
      }

    export default Home;
Enter fullscreen mode Exit fullscreen mode

In the code block above, we do the following:

  • Import the react-intersection-observer useView hook in our index.js file
  • Destructure the useView hook to get a ref that we assign to the element we want to monitor and an inView status to tell us if the element is present on the screen
  • Create two state variables, an appBlogRead variable to hold how many times the blog has been read per mount, which will not be more than one, and a blogCount variable to store how many times our blog has been read in general, we will update this variable with the value from our database

Next, in our index.jsx file, we pass our blogCount variable to the h3 element to display our blog reads.

    <h3>Blog reads: {blogCount}</h3>
Enter fullscreen mode Exit fullscreen mode

Understanding that we only want a read to count when a user has gone through 70 percent of the page, we add the ref to an element in the last quarter of our blog post. Doing this ensures that a read will not count unless that element has been on screen.

    <div className="blog">
      <div className="nav"></div>
      <div className="blog-container">
        <div className="writer">
          <div className="writer-image"></div>
          <div className="writer-name">Adut Elsesser</div>
        </div>
        <div className="blog-header">
          <h3>Blog reads: {blogCount}</h3>
          <h2>Mind on the road, your dilated eyes</h2>
          <p>Watch the clouds float, white Ferrari</p>
        </div>
        <div className="blog-image"></div>
        <div className="blog-story-container">
          <p>
            Had a good time
            (Sweet 16, how was I supposed to know anything?I let you out at CentralI didn't care to state the plainKept my mouth closedWe're both so familiarWhite Ferrari, good times
          </p>
          <p> 
            Stick by me, close by me You were fine You were fine here That's just a slow body You left when I forgot to speak So I text to speech, lesser speeds Texas speed, yes Basic takes its toll on me, Eventually, eventually, yes Ah, on me eventually, eventually, yes I care for you still and I will forever That was my part of the deal, honest We got so familiar Spending each day of the year, White Ferrari Good times In this life, life In this life, life

          </p>

          {/* The ref */}

          <p ref={myRef}> 
            One too many years Some tattooed eyelids on a facelift Mind over matter is magic I do magic If you think about it it'll be over in no time And that's life I'm sure we're taller in another dimension You say we're small and not worth the mention
          </p>
          <p>
            You're tired of movin', your body's achin' We could vacay, there's places to go Clearly this isn't all that there is Can't take what's been given But we're so okay here, we're doing fine Primal and naked You dream of walls that hold us imprisoned It's just a skull, least that's what they call it And we're free to roam
          </p>
        </div>
      </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Next, we use the useEffect hook to check if the element we are monitoring is in view and if our appBlogRead variable is less than one. If these statements are true, we want to add a read count to our appBlogRead and blogCount variable.

    import {useEffect, useState} from 'react';
    import { useInView } from 'react-intersection-observer';
    import {sdk, createAnonymousSession } from '../utils.js'

    const Home = () => {
      const { ref: myRef, inView } = useInView();
      const [appBlogRead, setAppBlogRead] = useState(0)
      const [blogCount, setBlogCount] = useState()

      useEffect(() => {
        if(inView){
          if (appBlogRead < 1) {
            setAppBlogRead(appBlogRead + 1)
            setBlogCount(blogCount + 1)
          }else{
            return
          }

        }
      }, [inView])

      return(
          <div></div>
      )
    }

    export default Home;
Enter fullscreen mode Exit fullscreen mode

Storing the view count to the database

In the index.js file, we create two functions to update and get information from our database.

    const handleBlogCount = async () => {
        try {
          let promise =  await sdk.database.listDocuments('collectionID')
          setBlogCount(promise.documents[0].blogCount)

        } catch (error) {
          console.log(error)
        }
    }

    const updateBlogCount = async () => {
      try {
          await sdk.database.updateDocument('collectionID', 'documentID', {
            "blogCount": blogCount
          })
          handleBlogCount()

        } catch (error) {
          console.log(error)
        }
    }

    useEffect(() => {
      createAnonymousSession()
      handleBlogCount()
    }, [])

      useEffect(() => {
        updateBlogCount()
      }, [blogCount])
Enter fullscreen mode Exit fullscreen mode

In the code block above, we do the following.

  • Create a handleBlogCount function that finds a collection using the collection ID parameter, this function updates the blogCount variable using its response
  • Create a updateBlogCount function that finds a document using its collection ID and document ID parameters,updateBlogCount function then updates the document using the blogCount variable, these collection and document ID are from the document we created at the start of this tutorial
  • Use the React useEffect to fire off the createAnonymousSession in our utils.js file and the handleBlogCount function once our application mounts
  • Runs our updateBlogCount function each time our blogCount variable is updated using React useEffect hook

We have created a robust blog post read counter in Next.js with this.

https://gist.github.com/Iheanacho-ai/d1eb08bea25f99d406e185a077a0c900

Here is how our blog post looks...

Appwrite

Read about 70 percent of the blog post to see the blog post read increase on the page and in the database.

Appwrite

Conclusion

This article discussed tracking blog reads with the react-intersection observer library, storing and retrieving these reads using an Appwrite database. As the post creates the base for an analytics implementation, update the code to restrict the blog count to only one read per user session.

Resources

Here are some resources that might be helpful:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .