by Francesco Agnoletto

How to add a trending section to your gatsby blog

Display the most popular pages on your website

Official Gatsbyjs logo

I always wanted to have a trending page in the blog to display the most viewed articles.
There are many ways to collect data, but getting the data back is not as easy. I found guides to set up servers, outdated plugins but nothing simple for the average website using Google analytics.

I’m using gatsby-plugin-google-analytics to get visitors data on my blog. Unfortunately, Google does not make it easy to understand how you are supposed to query the data it collects.

Additionally, the only plugin available for gatsby is poorly documented and has not been updated for the last year and a half. npm audit also highlights high vulnerabilities on the package from outdated dependencies.
Checking the code from the plugin, I found out it uses Google Reporting’s v3 API. Google’s ability to suddenly drop products is quite renowned, so I wanted to rely on the latest v4 API.

Making sure analytics works with the Reporting API

The first step, assuming Google analytics is already set up, is to make sure we can get data from it.
Google makes available the Request Composer, a simple form using the Analytics Reporting v4 API to show how to query data with it. Using your account, you should be able to query data from your site.

Request composer example query Example query requesting the page views for the last 30 days.
The viewId property can be found on your Google analytics account > admin > View Settings.

Make sure to launch the query and check the result to confirm it works as it should.

Creating a Viewer account

We need a Google Cloud account to be able to query the data successfully.

google-cloud-platform

The default users’ permissions do more than what we need.
I prefer to create a new account with just the Viewer role, it limits the scope of the account and is less of a problem if somehow the key gets exposed.

To create a new user, navigate to Service accounts from the sidebar and + Create Service Account.
Give it the name you want but make sure it has only the Viewer role.

service-account-viewer

Once this is done, in the third and last screen + Create Key and then Done.

There should be a new account with the Viewer role in the dashboard now. We will use the downloaded JSON file to authenticate with Google in our application.

Getting data from the Reporting API

Now we can start working on our gatsby project.

npm i dotenv googleapis will add dotenv and the Google API client to our repository to make sure we can use our data safely and query the analytics data.
Create an empty .env file at the root of the project and add inside the PRIVATEKEY, CLIENTEMAIL from the JSON and the VIEWID from Analytics. The .env should be .gitignore‘d, for production set the environment variables in the service you are using.

// gatsby-node.js
// google auth logic
const crypto = require("crypto");
const { google } = require("googleapis");

require("dotenv").config();

exports.sourceNodes = async ({ actions }) => {
  const { createNode } = actions;

  // google auth logic
  const scopes = "https://www.googleapis.com/auth/analytics.readonly";
  const jwt = new google.auth.JWT(
    process.env.CLIENT_EMAIL,
    null,
    process.env.PRIVATE_KEY,
    scopes
  );
  await jwt.authorize();

  const analyticsReporting = google.analyticsreporting({
    version: "v4",
    auth: jwt,
  });

  // Analytics Reporting v4 query
  const result = await analyticsReporting.reports.batchGet({
    requestBody: {
      reportRequests: [
        {
          viewId: process.env.VIEWID,
          dateRanges: [
            {
              startDate: "30DaysAgo",
              endDate: "today",
            },
          ],
          metrics: [
            {
              expression: "ga:pageviews",
            },
          ],
          dimensions: [
            {
              name: "ga:pagePath",
            },
          ],
          orderBys: [
            {
              sortOrder: "DESCENDING",
              fieldName: "ga:pageviews",
            },
          ],
        },
      ],
    },
  });

  // Add analytics data to graphql
  const { rows } = result.data.reports[0].data;
  for (const { dimensions, metrics } of rows) {
    const path = dimensions[0];
    const totalCount = metrics[0].values[0];
    createNode({
      path,
      totalCount: Number(totalCount),
      id: path,
      internal: {
        type: `PageViews`,
        contentDigest: crypto
          .createHash(`md5`)
          .update(JSON.stringify({ path, totalCount }))
          .digest(`hex`),
        mediaType: `text/plain`,
        description: `Page views per path`,
      },
    });
  }
};

Now run npm start and check localhost:8000__graphql there should be two new fields to query, pageViews and AllPageViews. The first takes an id (slug) as parameter, the second displays all pages.

graphql-pageviews-query Example allPageViews query.

Displaying the new data

Now that we can get our numbers from graphql, it is time to create a new route for it. I opted for a new route called /popular to display my articles sorted by views.

// src/pages/popular.tsx

import React from "react";
import { Link, graphql } from "gatsby";

import { ArticleCard } from "../components/styled";

interface IProps {
  data: {
    allMarkdownRemark: {
      edges: Array<{
        node: {
          excerpt: string;
          fields: {
            slug: string;
          };
          frontmatter: {
            date: string;
            title: string;
            description: string;
          };
        };
      }>;
    };
    allPageViews: {
      nodes: Array<{ id: string; totalCount: number }>;
    };
  };
}

const PopularPosts: React.FC<IProps> = ({ data }) => {
  const pageViews = data.allPageViews.nodes;

  // add the pageview data to the post objects
  const posts = data.allMarkdownRemark.edges.map(post => {
    const { slug } = post.node.fields
    const currentPageViews = pageViews.find(
      filteredPageViews => filteredPageViews.id === slug
    )?.totalCount;
    return {
      ...post,
      pageViews: currentPageViews || 0,
    };
  });

  return (
    <div>
      <h1>Most popular Articles</h1>
      {posts
        .sort((postA, postB) => postB.pageViews - postA.pageViews)
        .map(({ node }) => {
          const title = node.frontmatter.title || node.fields.slug;
          return (
            <ArticleCard key={node.fields.slug}>
              <small>{node.frontmatter.date}</small>
              <h3>
                <Link to={node.fields.slug}>{title}</Link>
              </h3>
              <p
                dangerouslySetInnerHTML={{
                  __html: node.frontmatter.description || node.excerpt,
                }}
              />
            </ArticleCard>
          );
        })}
    </div>
  );
};

export default PopularPosts;

export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: {
      fields: [frontmatter___date],
      order: DESC
    }) {
      edges {
        node {
          excerpt
          fields {
            slug
          }
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            description
          }
        }
      }
    }
    allPageViews(sort: { order: DESC, fields: totalCount }) {
      nodes {
        id
        totalCount
      }
    }
  }
`;

The npm package

Open source is the best part of the JavaScript ecosystem.

I created a gatsby plugin with the code from the Getting data from the Reporting API section. It can be used as an alternative to the code in gatsby-node.js.
Just follow the instructions in the readme and you should be able to set it up in no time.
It is my humble contribution to the amazing gatsby community.