- Published on
How I built a real-time blog view counter with NextJs and Firebase 👨💻
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 🛢️
- If you haven't created a firebase account, create one.
- 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.
- After creating project, navigate to "Realtime Database" and click "Create Database".
- 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).
- 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
inFIREBASE_DATABASE_URL
otherwise it will throw a strange error while developing.
- 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".
- Navigate to "Service Accounts" tab. Click and download the new private key. It should download a JSON file.
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-----MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDxpgRQF7jipkYFMkcCkfgw0gp3aVesEN0PM3jpQttNMVv+EBYnh0zqdKn/A+kQSnf9hA6YVq/zQufPemQ6wTvqv3uCsjk/ZYhX+15Ht9aEK007oOzOAWRMpIzARlMXpHkvSn6maTbMmNydJmA98lggjOV1DeLXBhg1Njgd7zxv/M8kDgsqJicRp43RbaFFwp0yeSY5+rQkrcXKJOTvia4OOCRAFbLOtnkqs5QFdv8DFHRaH4vhjrAxyq6QggLNeYzJ3Hgp5YlNf7QzrJGWT5UJmjtafdNGDTCgaNRDaZoKcjagcL8or14GCKJrNOEGn8Mzr2H66rWgV1mFDEzCXFOgnPTvbrdaFRECWuGA5CEwrlG1bDBxt88CgYBamvJZGKkcJLtBlRC1aN7rqHq5FTfFdagnCejHIHnA8N0SOgx2QhJSiwyU5QRSPlnfQLaI1X2FKnLhiMhOjFBKgDObkn4eBAKuwFkIhWMxEkkUqdRYUbT/O8IVyZHk0mCKhv+kQdtXI0+lstN/3LlOMUWlBYnTe64tSF4gauNjIQ==-----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.
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.
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} />
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.