Creating a Human vs AI Auto Blogger - Part 4 - Set up 'View Count' and 'Time to Read' with Upstash

Creating a Human vs AI Auto Blogger - Part 4 - Set up 'View Count' and 'Time to Read' with Upstash
Creating a Human vs AI Auto Blogger - Part 4 - Set up 'View Count' and 'Time to Read' with Upstash

Creating a Human vs AI Auto Blogger - Part 4 - View Count & Minutes to Read Post with Upstash

This is vendor lock-in with Upstash (free to start) for the small bonus of having a view count and an average ‘minutes to read’ calculator…which I kind of want!

I used Hosna’s post who was inspired by Andreas' blog post, which was inspired by Lee's blog. These provide a more in depth overview on how this works but the overview is:

  • Naviagate to /[slug]/page.tsx
  • ReportView component fires an API POST in app/api/increment/route.ts to our Upstash redis db
  • Upstash redis stores our view count
  • We read our view count at top of page
  • We calculate the read time based on number of words on page

Untitled.png

Let’s dive into it! The first thing we are going to do is get our API keys from Upstash, let’s go and create a Redis db.

Untitled.png

Untitled.png

Untitled.png

The free plan should be enough for our needs.

Untitled.png

Scroll down after you are back to the dashboard and copy your env keys

Untitled.png

Store these in your .env file

Now open up the terminal and install the npm package for Upstash:

npm i @upstash/redis

Once installed, let’s create our report view component which will be a function that returns null but tells redis to increment our view count via the api route we are about to create after. First make this component in /src/components/blog/report-view.tsx

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

Now let’s make the api increment route in /src/app/api/increment/route.ts

import { Redis } from "@upstash/redis";
 
export var config = {
  runtime: "edge",
};
const redis = Redis.fromEnv();
export async function POST(req: Request) {
  const body = await req.json();
  const { slug } = body;
  if (!slug) {
    return new Response("Slug is not provided", { status: 404 });
  }
  const ip = req.headers.get("x-forwarded-for") ?? "";
  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("");
 
  const isNew = await redis.set(["deduplicate", hash, slug].join(":"), true, {
    nx: true,
    ex: 24 * 60 * 60,
  });
  if(!isNew){
    return new Response('User has already viewed the page' , {status:202})
  }
  const res = await redis.incr(["pageviews", "projects", slug].join(":"));
  console.log('The number is : ' , res)
  return new Response('The user has successfully added to the viewer list', {status: 200})
  
}

Here we are checking that the IP address is actually unique/new, and if it is, we increment the pageviews in Redis.

We now have a component that fires an increment for new viewers every time they visit that page. Now lets set up the calculate read time and a function to format the date.

We add our format date and calculate read time functions in our /src/lib/utils.ts

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
 
 
export function formatDate(input: string | number): string {
  const date = new Date(input)
  return date.toLocaleDateString("en-US", {
    month: "long",
    day: "numeric",
    year: "numeric",
  })
}
 
export function reformatDate(dateStr: string) {
  // Split the input date string to get year, month, and day
  const parts = dateStr.split('-').map((part) => parseInt(part, 10));
 
  // Create a new Date object using UTC values
  // Note: The month argument is 0-indexed (0 is January, 1 is February, etc.)
  const date = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));
 
  // Correctly typed options for toLocaleDateString
  const options: Intl.DateTimeFormatOptions = {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    timeZone: 'UTC', // Specify UTC as the timezone
  };
 
  return date.toLocaleDateString('en-US', options);
}
 
export function calculateReadingTime(mdxContent: any) {
  // Define the average reading speed (words per minute)
  const wordsPerMinute = 200;
 
  // Strip MDX/HTML tags and count the words
  const text = mdxContent.replace(/<\/?[^>]+(>|$)/g, ''); // Basic stripping of HTML tags
  const wordCount = text
    .split(/\s+/)
    .filter((word: any) => word.length > 0).length;
 
  // Calculate reading time
  const readingTime = Math.ceil(wordCount / wordsPerMinute);
 
  return readingTime;
}

The calculate minutes to read is based on the number of words in the article divided by the average words a person can read per minute (200).

Next, let’s call these functions as well as our report views component within our /src/app/(blog)/portfolio/[slug]/page.tsx for our human made posts

import { Mdx } from '@/components/blog/mdx-wrapper'
import { ReportView } from '@/components/blog/view-report'
import { Button, buttonVariants } from '@/components/ui/button'
import { calculateReadingTime, cn, formatDate } from '@/lib/utils'
import { Redis } from '@upstash/redis'
import { allPortfolios, allPosts } from 'contentlayer/generated'
import { ChevronLeft, EyeIcon } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { notFound, redirect } from 'next/navigation'
import React, { FC } from 'react'
import { Metadata } from 'next';
 
 
interface PostProps {
  params: {
    slug: string
  }
}
 
 
async function getDocFromParams(slug: string){
  const doc = allPortfolios.find((doc) => doc.slug === slug)
 
  if(!doc) notFound()
 
  return doc
}
 
const redis = Redis.fromEnv();
 
 
 
const Post = async ({params}: PostProps) => {
 
 
  const doc = await getDocFromParams(params.slug)
 
  const views =
    (await redis.get<number>(['pageviews', 'projects', doc.slug].join(':'))) ??
    0;
 
       
  return (
    <div>
        <ReportView slug={doc.slug} />
      <div className='rounded-xl mx-auto max-w-5xl mt-10 relative'>
        <Image src={doc?.image.src} width={doc?.image.width} height={doc?.image.height} alt={doc?.title} className='rounded-xl z-0'/>
        <div className='absolute top-20 mx-20 text-center p-5 rounded-lg bg-white/20 z-10 text-slate-900 font-extrabold text-3xl'>{doc?.title}</div>
      </div>
      <div className='my-2 text-center'>
      {doc.date && (
            <time
              dateTime={doc.date}
              className="block ml-auto text-md text-muted-foreground"
            >
              <EyeIcon className="inline w-4 h-4 text-muted-foreground mr-1" />  
                {Intl.NumberFormat('en-US', { notation: 'compact' }).format(
                  views,
                )}{' '}
                {' views'}
               <span className="mx-1"></span> {` `} ·{" "} <span className="mx-1"></span>   {formatDate(doc.date)} <span className="mx-1"></span> {` `} ·{" "}
              <span className='mx-1'>
              {calculateReadingTime(doc.body.code)}
              {' min read'} 
            </span>
            </time>
          )}
      </div>
      
      
      <Mdx code={doc.body.code}/>
      
    </div>
  )
}
 
export default Post

and in /src/app/(blog)/posts/[slug]/page.tsx for our AI made blog posts.

import { Mdx } from '@/components/blog/mdx-wrapper'
import { ReportView } from '@/components/blog/view-report'
import { buttonVariants } from '@/components/ui/button'
 
import { calculateReadingTime, cn, formatDate } from '@/lib/utils'
import { Redis } from '@upstash/redis'
import { allPosts } from 'contentlayer/generated'
import { ChevronLeft, EyeIcon } from 'lucide-react'
 
import Link from 'next/link'
import { notFound, redirect } from 'next/navigation'
import React, { FC } from 'react'
import { toast } from 'sonner'
 
 
interface PostProps {
  params: {
    slug: string
  }
}
 
async function getDocFromParams(slug: string){
  const doc = allPosts.find((doc) => doc.slugAsParams === slug)
 
  if(!doc) notFound()
 
  return doc
}
 
const redis = Redis.fromEnv();
 
const Post = async ({params}: PostProps) => {
 
 
  const doc = await getDocFromParams(params.slug)
 
  const views =
    (await redis.get<number>(['pageviews', 'projects', doc.slug].join(':'))) ??
    0;
 
    
  return (
    <div>
        <ReportView slug={doc.slug} />
        <div className='text-center'>
        {doc.date && (
            <time
              dateTime={doc.date}
              className="block ml-auto text-md text-muted-foreground"
            >
              <EyeIcon className="inline w-4 h-4 text-muted-foreground mr-1" />  
                {Intl.NumberFormat('en-US', { notation: 'compact' }).format(
                  views,
                )}{' '}
                {' views'}
               <span className="mx-1"></span> {` `} ·{" "} <span className="mx-1"></span>   {formatDate(doc.date)} <span className="mx-1"></span> {` `} ·{" "}
              <span className='mx-1'>
              {calculateReadingTime(doc.body.code)}
              {' min read'} 
            </span>
            </time>
          )}
        </div>
      
      <Mdx code={doc.body.code}/>
    </div>
  )
}
 
export default Post
 
 

Now we should be seeing our view count change when we visit the page but not change on refresh.

Untitled.png

And that’s it folks!

Congrats on Finishing Part 4!

With that complete, you now have the a blog ready for your website using Notion & OpenAI.

This is just the start, subscribe for more courses on how to add to this project and build your next successful venture.