Pankaj Tanwar
Published on

How I built a real-time blog view counter with NextJs and Firebase 👨‍💻

––– views

I wanted to build a simple, free solution for tracking blog post views (with lazyload) on my website which is built with NextJs and deployed to vercel.

I was using Google Analytics to track how my blog is performing. Due to the rise of ad-blockers and for better accuracy and privacy for the readers, I decided to ditch my friend Google analytics to code an elegant solution to track blog views count.

This blog is a documentation of my journey to build this solution using NextJs + firebase and deploy it for absolutely free.

One of the challenges was to migrate old views count from google analytics to firebase. It deserves a separate blog post. I will cover it in upcoming blogs.

Firebase Setup 🛢️

  1. If you haven't created a firebase account, create one.
  2. Once you have signed-up, head over to the firebase console and create a new project. In next steps, enter project name and you may choose to enable google analytics for it.

Create Firebase Project

  1. After creating project, navigate to "Realtime Database" and click "Create Database".

Create Firebase Database

  1. While creating database, select any realtime database location. In next step, select "Start in test mode" (as it does not contain any sensitive info and we want the data to be available openly).

Database Config

  1. So now, we have created a free realtime database. Our next step is to get ENV variables. We need below 4 variables from firebase.
FIREBASE_PRIVATE_KEY=
FIREBASE_CLIENT_EMAIL=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
FIREBASE_DATABASE_URL=
  • FIREBASE_DATABASE_URL & NEXT_PUBLIC_FIREBASE_PROJECT_ID can be seen in below image.

Don't forget HTTPS in FIREBASE_DATABASE_URL otherwise it will throw a strange error while developing.

Two ENV Variables

  • Now, to get the remaining 2 env variables, in the left menu, at the top, besides "Project Overview", click on the "settings" icon and navigate to "project settings".

Project Settings

  • Navigate to "Service Accounts" tab. Click and download the new private key. It should download a JSON file.

Service Accounts

Download JSON file will have a lot fields like type, project_id, private_key_id, private_key, client_email, client_id, auth_uri, token_uri, auth_provider_x509_cert_url, client_x509_cert_url. We only need private_key and client_email.

So, private_key is FIREBASE_PRIVATE_KEY and client_email is FIREBASE_CLIENT_EMAIL. Remember, FIREBASE_CLIENT_EMAIL does not have HTTPS.

While deploying it to vercel, please convert '\n' to the actual new lines.

For example - in the previous JSON file, you will get private_key like this -

{
"private_key" : "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDxpgRQF7jipkYF\nMkcCkfgw0gp3aVesEN0PM3jpQttNMVv+EBYnh0zqdKn/A+kQSnf9hA6YVq/zQufP\nemQ6wTvqv3uCsjk/ZYhX+15Ht9aEK007oOzOAWRMpIzARlMXpHkvSn6maTbMmNyd\nJmA98lggjOV1DeLXBhg1Njgd7zxv/M8kDgsqJicRp43RbaFFwp0yeSY5+rQkrcXK\nJOTvia4OOCRAFbLOtnkqs5QFdv8DFHRaH4vhjrAxyq6QggLNeYzJ3Hgp5YlNf7Qz\nrJGWT5UJmjtafdNGDTCgaNRDaZoKcjagcL8or14GCKJrNOEGn8Mzr2H66rWgV1mF\nDEzCXFOgnPTvbrdaFRECWuGA5CEwrlG1bDBxt88CgYBamvJZGKkcJLtBlRC1aN7r\nqHq5FTfFdagnCejHIHnA8N0SOgx2QhJSiwyU5QRSPlnfQLaI1X2FKnLhiMhOjFBK\ngDObkn4eBAKuwFkIhWMxEkkUqdRYUbT/O8IVyZHk0mCKhv+kQdtXI0+lstN/3LlO\nMUWlBYnTe64tSF4gauNjIQ==\n-----END PRIVATE KEY-----\n"
}

On your local, make sure your include double quotes "private_key_value" for FIREBASE_PRIVATE_KEY. But while deploying it to vercel, in the vercel web app, env field FIREBASE_PRIVATE_KEY value should be -

-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDxpgRQF7jipkYF
MkcCkfgw0gp3aVesEN0PM3jpQttNMVv+EBYnh0zqdKn/A+kQSnf9hA6YVq/zQufP
emQ6wTvqv3uCsjk/ZYhX+15Ht9aEK007oOzOAWRMpIzARlMXpHkvSn6maTbMmNyd
JmA98lggjOV1DeLXBhg1Njgd7zxv/M8kDgsqJicRp43RbaFFwp0yeSY5+rQkrcXK
JOTvia4OOCRAFbLOtnkqs5QFdv8DFHRaH4vhjrAxyq6QggLNeYzJ3Hgp5YlNf7Qz
rJGWT5UJmjtafdNGDTCgaNRDaZoKcjagcL8or14GCKJrNOEGn8Mzr2H66rWgV1mF
DEzCXFOgnPTvbrdaFRECWuGA5CEwrlG1bDBxt88CgYBamvJZGKkcJLtBlRC1aN7r
qHq5FTfFdagnCejHIHnA8N0SOgx2QhJSiwyU5QRSPlnfQLaI1X2FKnLhiMhOjFBK
gDObkn4eBAKuwFkIhWMxEkkUqdRYUbT/O8IVyZHk0mCKhv+kQdtXI0+lstN/3LlO
MUWlBYnTe64tSF4gauNjIQ==
-----END PRIVATE KEY-----

Coding 🤖

Now, we have got all the variables so let's get our hands dirty in the actual coding.

Database Connection 🔌

To access the firebase realtime database, we will be using firebase-admin package. To install the package -

npm install firebase-admin

Under the lib folder, create a new file firebase.js . It handles the database connection.

import admin from 'firebase-admin'
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
project_id: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
private_key: process.env.FIREBASE_PRIVATE_KEY,
client_email: process.env.FIREBASE_CLIENT_EMAIL,
}),
databaseURL: process.env.FIREBASE_DATABASE_URL,
})
}
export default admin.database()

Implement views count 📈

In our database, we will be having views collection to store the count blog's slug wise. For database interaction, NextJs provides an excellent and simple solution that is NextJs API Routes.

With the help of NextJs API Routes, we will be incrementing the count of slug in the views collection for each blog visit.

Creating NextJs API Routes is easy-peasy. You just need to create api folder under pages folder in root directory. So, every file in api folder will behave as /api/*. So, I will create [slug].js at path /pages/api/views.

API Route 🚵

import db from '@/lib/firebase'
export default async (req, res) => {
// increment the views
if (req.method === 'POST') {
const ref = db.ref('views').child(req.query.slug)
const { snapshot } = await ref.transaction((currentViews) => {
if (currentViews === null) {
return 1
}
return currentViews + 1
})
return res.status(200).json({
total: snapshot.val(),
})
}
// fetch the views
if (req.method === 'GET') {
const snapshot = await db.ref('views').child(req.query.slug).once('value')
const views = snapshot.val()
return res.status(200).json({ total: views })
}
}

So, we have implemented 2 APIs. One is POST method, to increment the count for slug in views collection and one is to fetch the count.

To test, if it is working call the below API with POST method.

http://localhost:3000/api/views/this-is-blog-slug

you should be able to see the count of this blog.

Blog Count

Yayyyyy!!!!!!!! We are almost done. 🚀

Showing realtime views count 👀

So now, we have implemented the API routes to increment and fetch the views count. Next step is to hit API, get data and show it on the frontend.

For incrementing the count, we will use in-built beautiful fetch library and for data fetching, we will use yet another master piece library swr.

SWR is a react hook library for data fetching. It's lightweight and has an extremely smooth and smart way to fetch realtime data.

The most beautiful thing about SWR is, it first returns the data from cache and sends fetch call to revalidate the data. If you switch between tabs or refocus, it will update the views count in realtime. With SWR, components will get a stream of data updates constantly and automatically. And the UI will be always fast and reactive.

To install the package -

npm install swr

The next step is to create a views counter component that will increment and show the count.

So, the logic behind it is, this component will be placed on a blog page. Whenever a blog is opened, it gets rendered. It picks the blog slug and increments the counts in firebase.

import { useEffect } from 'react'
import useSWR from 'swr'
async function fetcher(...args) {
const res = await fetch(...args)
return res.json()
}
export default function ViewCounter({ slug }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher)
const views = new Number(data?.total)
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, {
method: 'POST',
})
registerView()
}, [slug])
return `${views > 0 ? views.toLocaleString() : '–'} views`
}

Intresting Problem 🤔

It was a straight forward solution. BUT I encountered an interesting situation. I needed to show the views count on the blogs page also.

Instresting Problems

When I put this component on blog page also, It was incrementing the views count which was wrong. So, to take this, I passed on an extra parameter that tells if this component was rendered on blog page or in a different page.

Updated code looks like this -

import { useEffect } from 'react'
import useSWR from 'swr'
async function fetcher(...args) {
const res = await fetch(...args)
return res.json()
}
export default function ViewCounter({ slug, blogPage = false }) {
const { data } = useSWR(`/api/views/${slug}`, fetcher)
const views = new Number(data?.total)
useEffect(() => {
const registerView = () =>
fetch(`/api/views/${slug}`, {
method: 'POST',
})
if (blogPage) {
registerView()
}
}, [slug])
return `${views > 0 ? views.toLocaleString() : '–––'} views`
}

Use View Counter Component

<ViewCounter slug={slug} blogPage={true} />

Views Counter

It was a fun exercise building this. Going forward, I will extend it to more accurate views count like based on IP address, within a specific time window, count the view as 1 only irrespective of page refresh.

If you have read till here, you might like my other writeups too. I am on twitter too, sharing my solo indie-hacking journey.