Creating a Blog - 11. Adding View Count to Posts

Table of Contents

Blog Creation Series

TitleLink
1. Basic Setuphttps://witch.work/posts/blog-remake-1
2. HTML Design for Main Pagehttps://witch.work/posts/blog-remake-2
3. Structure Design for Post Detail Pagehttps://witch.work/posts/blog-remake-3
4. Enabling Relative Paths for Imageshttps://witch.work/posts/blog-remake-4
5. Minor Page Composition Improvements and Deploymenthttps://witch.work/posts/blog-remake-5
6. Layout Design for Page Elementshttps://witch.work/posts/blog-remake-6
7. Main Page Component Designhttps://witch.work/posts/blog-remake-7
8. Design for Post List/Content Page Componentshttps://witch.work/posts/blog-remake-8
9. Automatic Generation of Post Thumbnailshttps://witch.work/posts/blog-remake-9
10. Design Improvements for Fonts, Cards, etc.https://witch.work/posts/blog-remake-10
11. Adding View Count to Postshttps://witch.work/posts/blog-remake-11
12. Page Theme and Post Search Functionalityhttps://witch.work/posts/blog-remake-12
13. Improvements to Theme Icons and Thumbnail Layoutshttps://witch.work/posts/blog-remake-13
14. Changing Post Categorization to Tag-Basedhttps://witch.work/posts/blog-remake-14
Optimizing Calculations on Main Pagehttps://witch.work/posts/blog-opt-1
Creating Pagination for Post Listhttps://witch.work/posts/blog-opt-2
Uploading Images to CDN and Creating Placeholdershttps://witch.work/posts/blog-opt-3
Implementing Infinite Scroll on Search Pagehttps://witch.work/posts/blog-opt-4

This post details the process of adding view counts to my new blog. I used some excerpts from the previous incomplete work on adding view counts for informational purposes.

In fact, if I had used Vercel, it would have been much easier to handle everything, but trying to do it on Cloudflare was extremely challenging. Eventually, I had to redeploy to Vercel.

There were numerous challenges and failures. If anyone wishes to deploy a NextJS app on Cloudflare Pages and add view counts... let’s hope Cloudflare Pages supports NodeJS runtime or properly supports SWR in the edge runtime.

However, reviewing the issues, the former appears to have no promise at all (it seems to be trying to run results built on Vercel on Cloudflare, which uses different underlying technologies) and the latter does not seem to be in particular plans. If anyone has succeeded, I would appreciate it if you could share your experience.

1. Moving Posts

First, I moved all the posts to the new blog. After moving, the build took much longer.

2. Busuanzi

There is a Chinese service called Busuanzi, which allows for easy addition of view counts to pages and blogs. This section includes excerpts from an older post of mine.

Following Fienestar's guide, with a few modifications to fit my blog.

First, add the following code to the site's head or body.

<script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>

My blog has a Seo component that is included on all pages. This Seo component is composed of the Helmet component from react-helmet, which manages the content in the head tag. (Note: In Next.js, you'll likely need to add this script via a Script tag in the Head component).

Thus, add the code in between the Helmet component.

<Helmet
// SEO metadata goes here.
// Skipping it as it is not important.
>
  <script async src='//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js'></script>
</Helmet>

2.1. Total Site Views and Unique Visitors

Total site views and unique visitors can be added with the following code. The id in the span is important.

<section style={{ height: '20px' }}>
  Total Views: <span id='busuanzi_value_site_pv'></span> times <br />
  Unique Visitors: <span id='busuanzi_value_site_uv'></span> people
</section>

This code needs to be added to the blog's page. In my blog, I added it right below my profile in the BlogIndex component, which represents the main page.

While struggling with this view count display, I thought a lot about restructuring the blog, hence I didn’t style it right away.

2.2. Page Views

The view count for a single page can be added with the following code.

<span id="busuanzi_value_page_pv"></span>

This has been suitably added just below the post title.

I’ll document other struggles below. I hope this knowledge will assist when I overhaul the blog in the future.

Remarkably, just two months later, I am rewriting this post. This time it’s in Next.js, hence the rewrite.

3. Google Analytics - Registration

3.1. Creating an Account

Let's create a new Google Analytics account.

create-account

Also, set up the website property.

attr-set

After inputting the business information, agree to the terms, and finish creating the account.

3.2. Migrating the Blog

Now that my blog is getting structured, let’s connect my witch.work domain to the new blog.

Currently, it’s connected to the blog page I created with Gatsby.

Access the Cloudflare Pages menu as follows.

cloudflare-pages

Then enter the project you were previously using and delete the custom domain menu for witch.work.

Add witch.work in the custom domain for witch-next-blog.

next-blog-custom-domain

3.3. Adding Data Stream and Tags

Next, enter the data stream menu and add a stream for the page.

data-stream

However, I see a warning stating that data collection has not been activated.

site-no-data

3.4. Setting the Tracking Code

To activate data collection, we need to register the Measurement ID obtained earlier. Let’s set the GA tracking code. Here, I received help from the front-end guru Lee Chang-Hee and Kim Min-Ji's blog.

Add the Google Analytics ID to blog-config.ts. It should be written as follows. The GA tracking code is the code that starts with G- found in Google Analytics.

Since this does not pose a security issue by being included in the git repo, it’s fine to write it in this file.

// blog-config.ts
const blogConfig: BlogConfigType = {
  name: 'Sung Hyun Kim',
  title: 'Witch-Work',
  description: 'I am not a person with outstanding ambitions. I’ve just followed the light emitted by remarkable people and arrived here, hoping to continue living that way. I feel honored to be able to share this place with you.',
  picture: '/witch.jpeg',
  url: 'https://witch-next-blog.vercel.app',
  social: {
    Github: 'https://github.com/witch-factory',
    BOJ: 'https://www.acmicpc.net/user/city'
  },
  thumbnail: '/witch.jpeg',
  googleAnalyticsId: 'G-XXXXXXXXXX' // This section should use your GA tracking code
};

Then create a script component for GA tracking. I took inspiration from ambienxo.

The script that inserts the GA tracking code is simply wrapped with next/script. Create src/components/GoogleAnalytics.tsx and write the following.

// src/components/GoogleAnalytics.tsx
import Script from 'next/script';

import blogConfig from '../../blog-config';

const GoogleAnalytics = () => {
  if (blogConfig.googleAnalyticsId == null) {
    return null;
  }
  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${blogConfig.googleAnalyticsId}`}
        strategy='afterInteractive'
      />
      <Script id='google-analytics' strategy='afterInteractive'>
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){window.dataLayer.push(arguments);}
          gtag('js', new Date());

          gtag('config', '${blogConfig.googleAnalyticsId}');
        `}
      </Script>
    </>
  );
};

export default GoogleAnalytics;

Then add this component to _app.tsx. It should be introduced here as it applies to all pages.

// _app.tsx
export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <meta name='viewport' content='width=device-width, initial-scale=1' />
        <link rel='manifest' href='/site.webmanifest' />
      </Head>
      <DefaultSeo {...SEOConfig} />
      <Header navList={blogCategoryList} />
      <Component {...pageProps} />
      <Footer />
      {/* Add it here */}
      <GoogleAnalytics />
    </>
  );
}

For those who wish to track view counts, please refer to this post. I failed due to the Cloudflare environment...

4. Attempting to Measure Views Using FirebaseDB

Referring to Real-time Blog View Count with NextJS and Firebase and a similar post, I attempted to measure view counts using a database.

In particular, using Google Analytics for view counts might lead to approximately 10% of counts being missed due to ad blockers. This is especially true for technology-related blogs, where most readers seem to use ad blockers.

4.1. Create a Firebase Project

Log into Firebase and go to the console. I logged in with my Google account. Then click 'Go to Console' in the top menu.

A screen will appear to create a project, so proceed to create a project.

create-project

I created a project named witch-blog-views. Although I could attach Google Analytics, I already had an account made previously.

4.2. Create the Database

Once your project is created, create a database. From the build category in the left menu, select Realtime Database.

make-db

On the resulting page, click Create Database. Select a database located in the US and start in test mode.

Then, click the gear icon next to 'Project Overview' in the upper left menu to go to project settings. Navigate to the Service accounts tab.

Click Generate new private key and save the resulting json file securely.

create-key

4.3. Connect the Database

Now, let’s connect to the database. Install firebase-admin.

npm i firebase-admin

Then create a .env.local file and add it to .gitignore, writing the following content.

NEXT_PUBLIC_FIREBASE_PROJECT_ID=replace-me
FIREBASE_CLIENT_EMAIL=replace-me
FIREBASE_PRIVATE_KEY="replace-me"

This information can be found in the downloaded json file by looking for similarly-named keywords. The PRIVATE_KEY value must be quoted.

Next, create src/lib/firebase.js and write the following code to initialize the app and establish the connection.

import * as admin from 'firebase-admin';
 
if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.cert({
      projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
      privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
    }),
  });
}
 
const db = admin.firestore();
 
export { db };

Now that we have established a connection with the database, we need to create a function in the API route that accesses the database to increment the view count for each HTTP request. I borrowed some ideas from this. Create api/views/[slug].js and write the following.

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 })
  }
}

Run in development mode with npm run dev. By sending a POST request to the /api/views/this-is-blog-slug address, you can verify that the view increases in the Firebase Realtime Database. I used Postman for the POST request, but any other method would work too.

4.4. Cloudflare Environment Issues

Now, let’s build it on Cloudflare. Note that you can also run it locally as it would behave on Cloudflare by executing the following commands.

npx @cloudflare/next-on-pages
# Run this command in a different terminal to execute the built results on local host.
npx wrangler pages dev .vercel/output/static --compatibility-flag=nodejs_compat

However, once I build it, I immediately get an error.

The following functions were not configured to run with the Edge Runtime:
⚡️ 		- api/views/[slug].func

Afterward, I keep encountering errors indicating that the runtime needs to be set to Edge. This is because NextJS's SSR uses Node.js runtime by default, which is not supported on Cloudflare Pages.

Thus, add the following line to api/views/[slug].js.

export const runtime = 'edge';

Now, a different error appears.

Dynamic Code Evaluation (e.g., 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime

This strongly suggests that something unsupported in edge runtime is being used. There was a related GitHub issue stating that firebase-admin requires full Node.js runtime, which is currently unsupported in Cloudflare workers.

Although a replacement package has been provided, it’s too difficult to implement, so I give up on this approach.

If anyone wants to use this method to add view counts, consider using this post as a reference to create a component that fetches view count data with SWR. But yet again, Cloudflare hindered me, so I will try a new method.

5. Measuring View Counts with Supabase

I decided to utilize Supabase, an open-source alternative to Firebase, which also supports edge functions.

5.1. Designing the View Count Counter

After much deliberation, let’s consider what is required for a view count counter when measuring views with a database. The features of the view count counter might include the following:

  1. When the page loads, it fetches and displays the page views from the serverless database.
  2. When the page loads, it increments the respective page view counter in the database.

This process should be done separately for each post. Thus, each entity in the database should contain the post title (as the post title is written in Korean, it will be used as the folder name containing the post) and its respective view count. The title should act as the primary key.

Communication with the database will use the API routes provided by Next.js, and the SWR library will be used to fetch the API route information. For a reference on how to use SWR with Next.js, check here.

Let’s attempt to set up the API route for fetching view counts.

Supabase Communication Logic

Although the database can be edited directly (as Supabase DB is easily editable through the web), this doesn’t undermine the importance of the tracking itself, so I believe this is sufficient.

5.2. Create a Supabase Project

First, I’ll create a Supabase project. Visit Supabase, log in with GitHub, and create a new project. The official documentation provides a friendly explanation of using Supabase with Next.js.

In the Project Page, create a new project. From my observations of the pricing policy, it seems to be more favorable than Firebase’s free policy. It’s reminiscent of Cloudflare Pages being more advantageous than Vercel for deployments... Anyway, fill in the information and create the project, selecting a region in Korea.

supabase project creation

Next, let’s create a table. In the SQL Editor, select your project and click Create table. Then enter the SQL below to create the views table, with the slug as the primary key and adding an integer for the view count and a timestamp indicating when the view count was recorded.

The int4 used in the view count is a 4-byte integer in Supabase. Using int2 allows storage up to 32,767 but I used int4 hoping that my posts would exceed 2^15 views someday. Who knows, perhaps the blog would see over 2 billion views.

create table views (
  slug text primary key,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  view_count int4
);

Next, I'll add a function to increase the view count using SQL.

create function increment (slug_text text)
returns void as
$$
update views
set view_count = view_count + 1
where slug = slug_text;
$$
language sql volatile;

Initialize the Supabase JavaScript client with the following command:

npm install @supabase/supabase-js

Then, write the following contents into the .env.local file, replacing replace-me with my project's URL and anon key. The project name and anon key can be found here.

SUPABASE_URL=replace-me
SUPABASE_KEY=replace-me

Next, create src/lib/supabaseClient.js with the following code.

import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
);

Then add these environment variables to the Cloudflare Pages settings. Select the project you created under Workers and Pages, then add the SUPABASE_URL and SUPABASE_KEY in the Settings - Environment Variables menu.

Setting Environment Variables in Cloudflare

5.3. Fetching View Count

Let’s create an API route to fetch view counts. Although one might think of creating api/views/[slug].js, there’s no such structure in the Cloudflare Pages deployment environment.

Cloudflare Pages supports only edge runtime for server components, which means all API routes will be treated as API endpoints rather than page routes. Therefore, dynamic API routes won't work. However, it is still necessary to fetch the data from the API routes, so let’s find a way to do this.

Testing one command to build in the Cloudflare environment step by step as follows:

# Command to test in the Cloudflare Pages build environment
npx @cloudflare/next-on-pages
npx wrangler pages dev .vercel/output/static --compatibility-flag=nodejs_compat

We will use the supabase object defined in src/lib/supabaseClient.js to create a function that fetches view counts. I managed to create it while referring to the official documentation. This will return the view_count from the views table, fetching only the row where the slug matches the function argument and using single to retrieve a single object as a response.

Since the slug acts as a primary key, the return value will either be none or one row, making the use of single appropriate.

// src/lib/supabaseClient.js
export async function getViewCount(slug) {
  const {data, error}=await supabase.from('views').select('view_count').eq('slug', slug).single();
  return data;
}

However, as mentioned earlier, dynamic API routes cannot be used in edge runtime. How will we pass the slug? By utilizing a query string in the GET request, as slugs are not particularly sensitive information.

We can then create api/view/index.ts and write the following.

// Active in edge runtime
export const runtime = 'edge';

import type { NextRequest } from 'next/server';

import { getViewCount } from '../../../lib/supabaseClient';

export default async function handler(
  req: NextRequest,
) {
  /* Extract the slug from the query string.
  Therefore, the query string should be written as ?slug=my-post-slug */
  const { searchParams } = new URL(req.url);
  const slug = searchParams.get('slug');
  
  /* In case slug is absent in the query string */
  if (!slug) {
    return new Response(
      'invalid slug in query string',
      {
        status: 400,
        headers: {
          'content-type': 'application/json',
        },
      }
    );
  }

  /* Fetch the view_count object using the slug from the query string.
  The return value should be {view_count : view_count_value} if a row matching the slug exists. */
  const data = await getViewCount(slug);

  return new Response(
    data?.view_count || 0,
    {
      status: 200,
      headers: {
        'content-type': 'application/json',
      },
    }
  );
}

How to test this function? I used the /about route for experimentation. I proceeded to create a component like this...

function View({slug}: {slug: string}) {
  const {data}=useSWR(`/api/view?slug=${slug}`);
  return <div>{`View Count: ${JSON.stringify(data)}`}</div>;
}

This is merely an intermediate iteration, hence I'll skip detailed explanations. Just experiment progressively using an unused route. The integration between SWR and Next.js can be referenced here.

5.4. Handling Errors in View Count Fetching

However, if a new user accesses a post whose view count doesn't yet exist, an error will arise. If the row corresponding to the slug does not exist, this will yield an error scenario. In testing, this resulted in data being returned as null. Let’s resolve this issue.

We’ll create a function that attempts getViewCount, and if an error occurs due to zero rows, it will insert the view count row for that slug.

The getViewCount should return both data and error.

// src/lib/supabaseClient.js
async function getViewCount(slug) {
  const {data, error} = await supabase
    .from('views')
    .select('view_count')
    .eq('slug', slug)
    .single();

  return {data, error};
}

Next, we create a function called registerViewCount that inserts a new row for a slug if it does not exist.

// src/lib/supabaseClient.js
export async function registerViewCount(slug) {
  await supabase
    .from('views')
    .insert({slug, view_count:1});
}

Now, let’s implement the fetchViewCount function that utilizes these functionalities.

// src/lib/supabaseClient.js
// Receives a slug and returns the view count for that slug
export async function fetchViewCount(slug) {
  const {data, error} = await getViewCount(slug);
  
  // If there is no row for the provided slug
  if (error) {
    if (error.details.includes('0 rows')) {
      /* Insert a new row */
      await registerViewCount(slug);
      const {data:newData, error:newError} = await getViewCount(slug);
      if (newError) {
        /* Handle error if it still occurs */
        return {data:null, error:newError};
      }
      else {
        return {data:newData, error:null};
      }
    }
    else {
      /* Handle other errors */
      return {data:null, error};
    }
  }
  return {data, error};
}

This function will then replace the former fetchViewCount.

// api/view/index.ts
import { fetchViewCount } from '../../../lib/supabaseClient';

export default async function handler(
  req: NextRequest,
) {
  const { searchParams } = new URL(req.url);
  const slug = searchParams.get('slug');
  if (!slug) {
    return new Response(
      'invalid slug in query string',
      {
        status: 400,
        headers: {
          'content-type': 'application/json',
        },
      }
    );
  }

  // Updated to use fetchViewCount
  const {data, error}=await fetchViewCount(slug);

  if (error) {
    return new Response(
      null,
      {
        status: 500,
        headers: {
          'content-type': 'application/json',
        },
      }
    );
  }

  return new Response(
    data?.view_count || 0,
    {
      status: 200,
      headers: {
        'content-type': 'application/json',
      },
    }
  );
}

Now, let’s create a component within src/pages/posts/[category]/[slug]/index.tsx to fetch the view count. This component will utilize useSWR for data fetching.

The fetch will involve adding an appropriate query string to api/view.

// src/pages/posts/[category]/[slug]/index.tsx
function ViewCounter({slug}: {slug: string}) {
  const {data}=useSWR(`/api/view?slug=${slug}`);
  return <div>{`View Count: ${data} times`}</div>;
}

To pre-fetch initial data into all SWR hooks, we use the fallback option of SWRConfig in getStaticProps.

// src/pages/posts/[category]/[slug]/index.tsx
export const getStaticProps: GetStaticProps = async ({params}) => {
  const post = getSortedPosts().find(
    (p: DocumentTypes) => {
      const temp = p._raw.flattenedPath.split('/');
      return temp[0] === params?.category && temp[1] === params?.slug;
    }
  )!;

  const {data} = await fetchViewCount(params?.slug);
  const fallback = {
    [`/api/view?slug=${params?.slug}`]: data?.view_count,
  };

  return {
    props: {
      post,
      fallback
    },
  };
};

Wrap the ViewCounter component in SWRConfig, passing the fallback object to ensure it has access to the initial values.

const slug = post._raw.flattenedPath.split('/')[1];
// additional code omitted for clarity
<SWRConfig value={{fallback}}>
  <ViewCounter slug={slug} />
</SWRConfig>

Now, the ViewCounter will always show the initial value from await fetchViewCount(params?.slug);, and when it makes a request to the API route, it will also pass back the view count to the component. This allows consistent updates to be displayed in the ViewCounter.

5.5. Aggregating View Counts

As per the current setup, every time a user opens a post, the view count increments by 1, but we still need to adjust for actual user interactions with posts. This can be handled in the ViewCounter component's useEffect.

Starting with restructuring the ViewCounter component into src/components/viewCounter/index.tsx to maintain functionality.

import useSWR from 'swr';

function ViewCounter({slug}: {slug: string}) {
  const {data:view_count}=useSWR(`/api/view?slug=${slug}`);
  return <div>{`View Count: ${view_count} times`}</div>;
}

export default ViewCounter;

Next, create a function to increase the view count for the respective slug in src/lib/supabaseClient.js, utilizing the previously defined increment function.

export async function updateViewCount(slug) {
  await supabase.rpc('increment', {slug_text:slug});
}

Update the index.ts file to handle a POST request that increments the view count.

export default async function handler(
  req: NextRequest,
) {
  /* omitted code for brevity */
  const {data, error} = await fetchViewCount(slug);
  /* Increment view count during POST requests */
  if (req.method === 'POST') {
    await updateViewCount(slug);
  }
  
  /* Further processing */
}

Update the ViewCounter component to send a POST request to increment the view count when the component renders.

import { useEffect } from 'react';
import useSWR from 'swr';

function ViewCounter({slug}: {slug: string}) {
  const {data:view_count}=useSWR(`/api/view?slug=${slug}`);

  useEffect(() => {
    fetch(`/api/view?slug=${slug}`, {
      method: 'POST',
    });
  }, [slug]);

  return <div>{`View Count: ${view_count} times`}</div>;
}

export default ViewCounter;

This implementation would increment the view count, but activating React's strict mode led to the view count increasing by 2. In strict mode, each component is rendered twice, thus causing double increments.

Disable React strict mode in next.config.js. Note that Cloudflare Pages' Next.js builds do not support React strict mode as of now, but it's a sensible adjustment.

const { withContentlayer } = require('next-contentlayer');

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    unoptimized: true,
  },
  reactStrictMode: false,
  swcMinify: false,
};

module.exports = (withContentlayer(nextConfig));

5.6. Returning to Vercel from Cloudflare...

I deployed this adjusted setup, and the view counts displayed correctly. However, the counts did not update in real-time. Why? Observing the developer tools' network tab showed that while fallback counts were working well, the real-time updates weren't happening.

SWR doesn’t seem to work effectively under Cloudflare. Dealing with a multitude of issues and trying various data-fetching methods, I eventually concluded that achieving real-time updates in edge runtime is nearly impossible, leading me back to using Vercel.

6. Redeploying to Vercel

6.1. Registering the Supabase Key

Having chosen to continue using Supabase, it is functional and a more generous free plan is available compared to Firebase.

Go into your Vercel project settings, and under Environment Variables, add the SUPABASE_URL and SUPABASE_KEY created earlier.

Registering Environment Variables in Vercel

6.2. Rewriting the Files

The src/lib/supabaseClient.js file does not require immediate changes.

The api/view/index.ts can be rewritten more concisely using NextApiResponse.

// src/pages/api/view/index.ts
import { NextApiRequest, NextApiResponse } from 'next';

import { fetchViewCount, updateViewCount } from '../../../lib/supabaseClient';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const slug = req.query?.slug?.toString();

  if (!slug) {
    return res.status(400).json({error: 'invalid slug in query string'});
  }

  if (req.method === 'POST') {
    await updateViewCount(slug);
  }

  const {data, error} = await fetchViewCount(slug);
  
  if (error) {
    return res.status(500).json({error});
  }
  return res.status(200).json({view_count:data?.view_count || 0});
}

The ViewCounter component will be simplified as follows.

// src/components/viewCounter/index.tsx
import { useEffect } from 'react';
import useSWR from 'swr';

const fetcher = async (input: RequestInfo) => {
  const res: Response = await fetch(input);
  return await res.json();
};

function ViewCounter({slug}: {slug: string}) {
  const {data}=useSWR(`/api/view/${slug}`, fetcher);
  
  useEffect(() => {
    fetch(`/api/view/${slug}`, {
      method: 'POST',
    });
  }, [slug]);

  return (
    <div>
      {`View Count: ${data?.view_count ?? '---'} times`}
    </div>
  );
}

export default ViewCounter;

With these updates, the view counts can be accurately fetched and displayed for each post.

7. Styling the ViewCounter

Let’s add some styling to the ViewCounter. First, set the font size to 1.25rem. Since this is simple, I’ll skip detailing that.

Next, align it with the post date, adding a thin gray line between the date and view count.

Within src/pages/posts/[category]/[slug]/index.tsx, structure the component around ViewCounter like this.

// src/pages/posts/[category]/[slug]/index.tsx
<div className={styles.infoContainer}>
  <time className={styles.time} dateTime={toISODate(dateObj)}>
    {formatDate(dateObj)}
  </time>
  <div className={styles.line}></div>
  <ViewCounter slug={slug} />
</div>

Styling for each component can be added straightforwardly.

// src/pages/posts/[category]/[slug]/styles.module.css
.infoContainer {
  display: grid;
  grid-template-columns: auto 1fr auto;
  margin-bottom: 0.5rem;
}

.line {
  margin: auto 0.5rem;
  border: 1px solid var(--gray1);
  height: 0;
}

This approach aligns the view count with the date while allowing for real-time updates. It appears that not using fallback values is crucial for real-time functionality.

References

[Blog Links and Resources]