📈

microCMS + Gatsbyで 記事のPV数ランキング を作成する

2022/10/24に公開

※この記事は以前ちょっと株式会社 社員ブログで公開していたものです

こんにちわ、フロントエンドエンジニアのしまむーです!
Jamstack構成のサイトを構築していると動的に近い機能を実装したい場合というのが出てくるかと思います。
今回はGoogle Analytics Reporting APIから取得してきたデータを使ってブログ記事の閲覧数ランキングを作成したいと思います。
他のAPIへの利用にも応用できると思いますので、ぜひこちらを参考に色んな機能の実装を行なってください。

構成

CMSなどから取得してきたデータなどを元に比較的簡単に静的コンテンツの生成をしてくれるReactベースのフレームワークです。
GatsbyにはmicroCMSさんがデータ取得してくるためのプラグインを用意してくれているのでそちらを使ってデータの取得を行います。
Google Analytics Reporting APIから取得してきたPV数はGatsbyで取り込んで扱えるようにします。

利用しているサービスやフレームワークについて

Gatsby

  • CMSなどから取得してきたデータなどを元に比較的簡単に静的コンテンツの生成をしてくれるReactベースのフレームワークです。
  • gatsby-source-microcmsというmicroCMSから簡単にデータを取得してくるためのプラグインが用意されている
  • Google Analytics Reporting APIから取得してきたPV数はGatsbyで取り込んで扱えるようにします

Google Analytics Reporting API

  • Google Analyticsのデータを柔軟に取得出来るAPI、ページ別や日別のPV(アクセス数)やユーザー数、直帰率、離脱率、平均ページ滞在時間、AdSense収益・クリック数...等を取得&Sortできます。
  • 今回の構成ではGatsbyのビルド時にNodeとして取り込んで静的コンテンツとして扱います。

Reporting APIから取得してきたJsonをNodeとして取り込む

Gatsby Node APIsに用意されているsourceNodesライフサイクルを利用してGoogle Analytics Reporting APIで取得してきたPV数をNodeとして取り込みます。
実際のコードは下記の通りになります。

const path = require('path');
const { google } = require('googleapis');

exports.createSchemaCustomization = ({ actions, schema }) => {
  const { createTypes } = actions;

  createPageRank();
  // `IPageRank` と `PageRank` を作成する
  function createPageRank() {
    createTypes(`
      interface IPageRank @nodeInterface {
        id: ID!
        path: String!
        title: String!
        count: Int!
      }
    `);

    createTypes(
      schema.buildObjectType({
        name: `PageRank`,
        fields: {
          id: { type: `ID!` },
          path: { type: `String!` },
          title: { type: `String!` },
          count: { type: `Int!` },
        },
        interfaces: [`Node`, `IPageRank`],
      })
    );
  }
};

exports.sourceNodes = async ({ actions, createNodeId, createContentDigest, reporter }) => {
  const { createNode } = actions;

  // 配列の片方をキー、片方を値にしたオブジェクトを作成する
  const mapFromArray = (a1, a2) => {
    let valueMap = {};
    for (let i = 0, length = Math.min(a1.length, a2.length); i < length; i++) {
      let v1 = a1[i],
        v2 = a2[i];
      valueMap[v1] = v2;
    }

    return valueMap;
  };

  await addPageRankNodes();

  // `PageRank` のノードを追加する
  async function addPageRankNodes() {
    const scopes = 'https://www.googleapis.com/auth/analytics.readonly';

    // 認証に必要な情報を設定
    const jwt = new google.auth.JWT(process.env.GCP_CLIENT_EMAIL, null, process.env.GCP_PRIVATE_KEY.replace(/\\n/gm, '\n'), scopes);

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

    // 認証
    await jwt.authorize();

    // 下記で API にリクエストをかけてデータを取得する
    // ここでは以下の条件でデータを取得しています
    const res = await analyticsreporting.reports.batchGet({
      requestBody: {
        reportRequests: [
          {
            viewId: process.env.GCP_VIEW_ID,

            // 期間: 30 日前から当日まで
            dateRanges: [
              {
                startDate: '30daysAgo',
                endDate: 'today',
              },
            ],

            // ページパスとページタイトルを基準にする
            dimensions: [
              {
                name: 'ga:pagePath',
              },
              {
                name: 'ga:pageTitle',
              },
            ],

            // ページビュー数を指標にする
            metrics: [
              {
                expression: 'ga:pageviews',
              },
            ],

            // ページパスが `/information/` から始まるものに限定。
            filtersExpression: `ga:pagePath=~^/information/`,

            // 並び順: ページビュー数の降順
            orderBys: {
              fieldName: 'ga:pageviews',
              sortOrder: 'DESCENDING',
            },

            // 最大取得件数、PV数を各ページの詳細ページへ表示したいので今回は2000件にしておく
            pageSize: 1000,
          },
        ],
      },
    });


    // データが取得できない場合は終了させる
    if (res.statusText !== 'OK') {
      reporter.panic(`Reporting API response status is not OK.`);
      return;
    }

    const [report] = res.data.reports;
    const dimensions = report.columnHeader.dimensions;
    const rows = report.data.rows;

    for (const row of rows) {
      let valueMap = mapFromArray(dimensions, row.dimensions);
      let data = {
        path: valueMap['ga:pagePath'],
        title: valueMap['ga:pageTitle'],
        count: parseInt(row.metrics[0].values[0], 10),
      };

      let nodeMeta = {
        id: createNodeId(`PageRank-${data.path}`),
        parent: null,
        children: [],
        internal: {
          type: `PageRank`,
          contentDigest: createContentDigest(data),
        },
      };

      let node = Object.assign({}, data, nodeMeta);

      if (valueMap['ga:pagePath'].slice(-1) === '/') {
        createNode(node);
      }
    }
  }
};

ランキングの表示を作成する

簡易的な表示ですが、実際に作成したものが下記の画像のページになります。

閲覧数上位3つのページのタイトルとPV数を表示させています。
実際のコードは下記の通りです。

src/pages/index.js

import * as React from 'react';
import { Link, graphql } from 'gatsby';
import { Layout } from '../components/layout';
import { PostRankingWidget } from '../components/post-ranking-widget';
import { dayjs } from '../lib/dayjs';

const HomePage = ({ data }) => {
  return (
    <>
      <Layout title="TOP">
        <section>
          <h2>記事一覧</h2>
          <ul>
            {data.allMicrocmsInformation.edges.map(({ node }) => (
              <li key={node.informationId}>
                <Link to={`/information/${node.informationId.replace(/(_)/g, '-')}/`}>
                  <span>{node.title}</span>
                  <small>{dayjs(node.date).format('YYYY/MM/DD')}</small>
                </Link>
              </li>
            ))}
          </ul>
        </section>
        <div>
          <PostRankingWidget data={data} />
        </div>
      </Layout>
    </>
  );
};

export default HomePage;

export const query = graphql`
  query {
    allPageRank(sort: { fields: [count], order: DESC }) {
      nodes {
        id
        path
        title
        count
      }
    }

    allMicrocmsInformation {
      edges {
        node {
          informationId
          title
          date
          body
          thumbnail {
            url
          }
          category {
            name
          }
        }
      }
    }
  }
`;

src/components/post-ranking-widget.js

import React from 'react';
import { Link } from 'gatsby';

export const PostRankingWidget = ({ data }) => {
  const informationPosts = data.allMicrocmsInformation.edges;
  const rankingPosts = data.allPageRank.nodes;

  const filteredRank = rankingPosts.filter((post) => {
    for (let information of informationPosts) {
      if (`/information/${information.node.informationId.replace(/(_)/g, '-')}/` === post.path) {
        post.title = information.node.title;
        return true;
      }
    }
    return false;
  });
  if (rankingPosts < 1) {
    return false;
  }
  return (
    <section>
      <h2>記事ランキング</h2>
      <ol>
        {filteredRank.slice(0, 3).map((post) => (
          <li key={post.id}>
            <Link to={post.path}>
              <strong>{post.title}</strong> <small>(PV: {post.count})</small>
            </Link>
          </li>
        ))}
      </ol>
    </section>
  );
};

これでデータを取得して表示させるまでの実装は完了になります、しかしこのままだと問題があります。
現状だと記事が投稿されてGatsbyにビルドが走るまで記事のPV数の更新がされません。

Github Actionsのscheduleイベントをトリガーを使う

記事のPV数が更新されない問題に対応するためにGithub Actionsのschedueトリガーを利用しました、1日1回必ずビルドされるようにしています、下記が実際のワークフローのコードです。

name: ScheduleDeploy # GitHub Actionsにつける名前です。

on:
  schedule:
    - cron: '0 3 * * *' # cronで定期実行する

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      SERVICE_ID: ${{ secrets.SERVICE_ID }}
      API_KEY: ${{ secrets.API_KEY }}
      GCP_VIEW_ID: ${{ secrets.GCP_VIEW_ID }}
      GCP_CLIENT_EMAIL: ${{ secrets.GCP_CLIENT_EMAIL }}
      GCP_PRIVATE_KEY: ${{ secrets.GCP_PRIVATE_KEY }}
    steps:
      - uses: actions/checkout@master

      - uses: actions/setup-node@v2
        with:
          node-version: 14.x

      - run: npm i
      - run: npm run build
      - run: npx netlify-cli deploy --dir=./public --prod
      - uses: actions/checkout@v2

以上で問題がなければ1日1回PV数の更新されるようになります。

最後に

いかがでしたでしょうか、今回実際に実装してみてJamstack構成のサイトでもリアルタイム更新が必要ない限り様々な動的機能を実装できるなと感じました、ぜひ皆さんも、色んな機能の実装に挑戦してみてください。

chot Inc. tech blog

Discussion