DEV Community

Cover image for Adding view count to your Nextjs Blog
Navin Kodag
Navin Kodag

Posted on

Adding view count to your Nextjs Blog

Alright lads, this will be a quick one. I want to add the views count functionality on my personal portfolio website's blog section.

ready.gif

Expected behavior:

  • Blogs Page : List of blogs -> Show count.
  • Blog Page : Particular Article -> Show count and Increment count.

How to achieve:

  • Use supabase to store count by slug
  • Stored procedure to increment count

Tools that I'll need:

  • supabase : open source firebase alternative
  • swr : data fetching

Setting up supabase table :

Create a table views with schema like such:

  • slug -> text -> primary key
  • created_at -> timestamp -> now()
  • count -> int2

Updating count:

  • Fetch count
  • Increment one
  • Fetch count again

Now we can reduce this to one db call using stored procedures:
stored-procedure.png

create function increment (slug_text text)
returns void as
$$
update views
set count = count + 1
where slug = slug_text;
$$
language sql volatile;
Enter fullscreen mode Exit fullscreen mode

In NextJs:

We'll define a route for ease:
- /api/view/{slug}
and then we'll use the POST request to register a view and GET to increment the view count.
Our handler code will look like this:
views.ts

import { createClient, PostgrestError } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY
);
interface SupabaseResult {
    data?: { count: number };
    error?: PostgrestError;
}
///
const getViews = async (slug: string): Promise<number> => {
const { data: views, error }: SupabaseResult = await supabase
.from("views")
.select(`count`)
.match({ slug: slug })
.single();
if (error && error.details.includes(`0 rows`)) {
    const { data, error }: SupabaseResult = await supabase
    .from(`views`)
    .insert({ slug: slug, count: 1 }, { returning: `representation` })
    .single();
    return data.count;
    }
    if (!views) {
    return 0;
    }
    return views.count;
    };
///
    const registerView = async (slug: string): Promise<void> => {
    const { data, error } = await supabase.rpc("increment", {
    slug_text: slug,
    });
};
export { getViews, registerView };
Enter fullscreen mode Exit fullscreen mode
  • /api/view/[slug].ts
// /api/view/[slug].ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { getViews, registerView } from "lib/views";
import type { NextApiRequest, NextApiResponse } from "next";
interface Data {
    message?: string;
    status?: number;
    count?: number;
}

///
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
): Promise<void> {
const slug = req.query.slug.toString();
///
if (!slug) {
    return res.status(400).json({ message: `invalid slug` });
}
if (req.method == `POST`) {
    await registerView(slug);
}
const count = await getViews(slug);
return res.status(200).json({ count: count });
}
Enter fullscreen mode Exit fullscreen mode

ViewCounter Component

  • view_counter.tsx
import fetcher from "lib/fetcher";
import { Views } from "lib/types";
import { useEffect } from "react";
import useSWR from "swr";

interface Props {
    slug: string;
}

const ViewCounter = ({ slug }: Props) => {
const { data } = useSWR<Views>(`/api/views/${slug}`, fetcher);
useEffect(() => {
    const registerView = () =>
    fetch(`/api/views/${slug}`, {
        method: "POST",
    });
    registerView();
}, [slug]);

return (
    <span>{`${
    (data?.count ?? 0) > 0 ? data.count.toLocaleString() :"–––"
    } views`}</span>
    );
};

export default ViewCounter;
Enter fullscreen mode Exit fullscreen mode

Our views in action:
view_counter.png
view_count.png

The code of this project lives at : https://github.com/100lvlmaster/100lvlmaster.in


You can find me at: https://100lvlmaster.in

Discussion (1)

Collapse
alanph profile image
Alan Phan

error - lib/views.ts (20:29) @ getViews
TypeError: Cannot read properties of null (reading 'includes')
18 | .single();
19 |

20 | if (error && error.details.includes(0 rows)) {
| ^
21 | const { data, error }: SupabaseResult = await supabase
22 | .from(views)
23 | .insert({ slug: slug, count: 1 }, { returning: representation })