Back
6min
100 views

Adding a View Counter in your Next.js blog using Upstash Redis

In this post, I'll show you how to add a view counter to your Next.js blog using Upstash Redis. We'll focus on two main goals: (1) displaying the number of views for each blog post, and (2) incrementing the view count each time someone visits a page. This builds on my previous post about creating a blog using contentlayer. Let's dive in and enhance your blog with this interactive feature!

Setting up Upstash Redis

  1. Create an Upstash account.
  2. Create a new Redis database.
  3. In the REST API section, click the .env button to copy your environment variables.
UPSTASH_REDIS_REST_URL=""
UPSTASH_REDIS_REST_TOKEN=""

Configuring Upstash

Install the Upstash Redis package in your Next.js app:

npm install @upstash/redis

Implementing the View Counter

Create an API route to handle saving user visits in the Redis database:

/app/api/increment/route.ts
import { Redis } from '@upstash/redis'
import { NextResponse, type NextRequest } from 'next/server'
 
const redis = Redis.fromEnv()
export const runtime = 'edge'
 
export async function POST(req: NextRequest): Promise<NextResponse> {
  const body = await req.json()
  let slug: string | undefined = undefined
  if ('slug' in body) {
    slug = body.slug
  }
  if (!slug) {
    return new NextResponse('Slug not found', { status: 400 })
  }
 
  const ip = req.ip
  if (ip) {
    // Hash the IP in order to not store it directly in your db.
    const buf = await crypto.subtle.digest(
      'SHA-256',
      new TextEncoder().encode(ip)
    )
    const hash = Array.from(new Uint8Array(buf))
      .map((b) => b.toString(16).padStart(2, '0'))
      .join('')
 
    // deduplicate the ip for each slug
    const isNew = await redis.set(['deduplicate', hash, slug].join(':'), true, {
      nx: true,
      ex: 24 * 60 * 60,
    })
 
    if (isNew) {
      await redis.incr(['pageviews', 'blog', slug].join(':'))
      return new NextResponse(null, { status: 202 })
    } else {
      return new NextResponse('View already counted', { status: 202 })
    }
  }
 
  return new NextResponse(null, { status: 202 })
}

This API route does the following:

  1. Extracts the slug from the request body.
  2. Hashes the visitor's IP address for privacy.
  3. Uses Redis to deduplicate views based on the hashed IP and slug.
  4. Increments the view count only for new views *not seen in the last 24 hours.

Redis provides two key commands that make this process efficient:

  • SET with the NX option: Sets a key only if it doesn't exist, perfect for deduplication.
  • INCR: Atomically increments a counter, ideal for tracking views. Redis provides two key commands that make this process efficient:

Reporting Views

Create a client-side component to call the API when a user visits a blog post:

/app/blog/[...slug]/report-view.tsx
'use client'
 
import { useEffect } from 'react'
 
export const ReportView: React.FC<{ slug: string }> = ({ slug }) => {
  useEffect(() => {
    void fetch('/api/increment', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ slug }),
    })
  }, [slug])
 
  return null
}

Displaying View Count

Create another component to fetch and display the view count for each blog post:

/app/blog/[...slug]/show-view.tsx
'use client'
 
import { useEffect } from 'react'
 
export const ReportView: React.FC<{ slug: string }> = ({ slug }) => {
  useEffect(() => {
    void fetch('/api/increment', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ slug }),
    })
  }, [slug])
 
  return null
}

Import and use these components in your blog post page:

/app/blog/[...slug]/page.tsx
import { allPosts } from 'contentlayer/generated'
import { notFound } from "next/navigation";
import { Suspense } from "react";
import { ReportView } from "./report-view";
import ShowViews from "./show-view";
 
// ...
 
export default function Blog({ params }) {
  // ...
 
  return (
    <section className="flex items-center justify-center w-full flex-col p-8">
      <ReportView slug={post.slug} />
      <div className="w-full max-w-3xl">
        <h1 className="title font-medium text-2xl md:text-4xl tracking-tighter font-heading">
          {post.title}
        </h1>
        <Suspense fallback={<div className="blur-sm">100 views</div>}>
          <ShowViews slug={post.slug} />
        </Suspense>
 
        {/* ... */}
      </div>
    </section>
  );
}

Folder Structure


route.ts
page.tsx
report-view.tsx
show-view.tsx
page.tsx

That's it! You've successfully implemented a view counter in your Next.js application. The view count will increment once every 24 hours per unique visitor for each blog post, and the updated count will be displayed on the page.

Refferences