Vue.js初心者がWP REST APIとGA Reporting APIを使ってスワブロランキングアプリを作ったよ
2020.04.01

ども、むったんです。
コロナ騒ぎで、ライブ&イベントが軒並み中止になってとても辛い。
えぇ、分かってます。
ウイルスを蔓延させないためって分かってます。
分かってる…けどさぁーーー!推しに会いたいよーーー😭
ちなみに3月に開催予定だったライヴは中止となり、払い戻しが始まりました😭
ライブが開催できていれば、リアル推し×画面の中にいた2D推しが3Dになってステージで歌って、踊っていたと思うと…是非もう一度企画していただきたいと思う所存!
むったんが行きたかったライブはこちら
そして、そんな悶々とした気持ちをぶつけながら作ったのが、今回のアプリですww
スワブロランキングアプリを作った
作り始めた経緯
みなさんが見てくれているこのスワローブログ(略してスワブロ)ですが、運用が始まったのは2018年10月。
そこから月ごと&四半期ごとに、中村さんがGoogleアナリティクスからフィルターかけたりしてランキングを発表してくれていました。
正直この作業簡略化できそうだし、アプリにできたら中村さんの仕事の負担ちょっとは減るよなー…と思っていました。
なので、リーダー千に「中村さんの仕事の負担を減らすために、WPとアナリティクスからデータ引っ張ってきてスワブロランキングアプリ作ってええ?( ´ ▽ ` )ノ」とノリで言ったらOKをもらいました!
ということで、ざっくり書いていくよ😆
環境
- WordPress 5.3.2
- GoogleアナリティクスReporting API v4
- Vue.js 2.6.10
- vuex 3.1.0
- vuetify 2.1.0
取得したいもの
GoogleアナリティクスReporting API v4
- URL
- PV数
WP REST API
- URL
- 投稿日
- ブログタイトル
- 投稿者
投稿者情報が不要であれば、GoogleアナリティクスReporting APIのみで行けたのですが…そりゃそうね、複数人で運用しているブログだもの。
投稿者は知りたいよね。
インストールしたもの
- vuetify
詳しい導入方法は以前むつたくが紹介しているので、そちらをご覧ください。
やっぱりフレームワーク使うとラクだわ(諦めれば、cssとの戦いも放棄できると最近踏ん切りがついたw
Vue-CLI3から始めるUIフレームワーク 〜Vuetify〜
- moment.js
インストール方法は以下の記事が分かりやすいです。
Vue.jsで日付処理ライブラリMoment.jsを使う|WEB PIXEL
- vue-monthly-picker
年月だけ指定したかったので、以下のものを使用させていただきました🙏
- vue-google-api
Google ApiとGoogle認証を使用して、クライアント側の操作を行うのに必要なもの。
ざっくり流れはこんな感じ

WP REST APIは投稿順で情報をくれるっぽい。
GA Reporting APIは好きなようにソートできるっぽいので、PV昇順に指定。
for文で回して、GA Reporting APIから来た内容を基準にWP REST APIの情報もまとめて、ランキングを表示しました。
準備(WordPress)
WordPress4.7以上であれば、プラグインなどを使用せずに下記URLから記事情報等がjson形式で取得できます。
http://[ドメイン]/wp-json/wp/v2/[slug]
ちなみに、今回はスワブロの情報を取得したいので、こんな感じになります。
https://swallow-incubate.com/wp-json/wp/v2/blog
取得件数の初期値は10件です。
準備(Google API)
設定が面倒だったのはこっちだった…orz
そして未だちゃんと理解してないけど、動いてるから大丈夫なのかな😅
ざっくり説明すると、こんな感じ。
- Google API ConsoleからGoogle Analytics Reporting APIを選択
- プロジェクトを作成
- 認証情報を作成
- Google Analyticsにサービスアカウントを追加
- APIキーを発行
- OAuth2.0クライアントIDを発行(むったんはhttp://localhost:8080を許可、状況に応じて変更してください)
詳しいやり方はSAKI Web Design様のブログに書いてありますので、参考にしてみてください🙏
Google Analytics Reporting API V4 を使う①|SAKI Web Design
コード
※コードが長くなるので、CSSは書きません。
src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/index'
import vuetify from './plugins/vuetify'
import VueGoogleApi from 'vue-google-api'
const config = {
clientId: '[clientId]',
scope: 'https://www.googleapis.com/auth/analytics',
apiKey: '[apiKey]',
discoveryDocs: ['https://analyticsreporting.googleapis.com/$discovery/rest?version=v4']
}
Vue.config.productionTip = false
Vue.use(VueGoogleApi, config)
new Vue({
router,
store,
vuetify,
render: h => h(App)
}).$mount('#app')
src/App.vue
<template>
<v-app>
<BaseLine />
<v-content>
<router-view />
</v-content>
</v-app>
</template>
<script>
import BaseLine from '@/components/BaseLine.vue'
export default {
name: 'App',
components: {
BaseLine
},
data: () => ({
//
}),
};
</script>
src/components/BaseLine.vue
<template>
<div id="baseLine">
<v-navigation-drawer
v-model="drawer"
app
>
<div class="navi-box">
<v-list mandatory>
<v-list-item-group v-model="navi">
<v-list-item
v-for="(item, i) in items"
:key="i"
:to="item.link"
class="d-flex"
>
<v-list-item-icon>
<v-icon v-text="item.icon" color="white"></v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title v-text="item.text" class="font-weight-bold"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
</div>
</v-navigation-drawer>
<v-app-bar
app
color="white"
dark
>
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
<v-toolbar-title class="title font-weight-bold">Swallow Blog Ranking</v-toolbar-title>
</v-app-bar>
</div>
</template>
<script>
export default {
name: 'BaseLine',
props: {
source: String,
},
data: () => ({
drawer: null,
navi: null,
item: 1,
items: [
{ text: '1ヶ月ランキング', icon: 'mdi-calendar', link: '/' },
{ text: '3ヶ月ランキング', icon: 'mdi-calendar-multiple', link: '/three-months' }
]
}),
}
</script>
src/views/Home.vue
<template>
<div
id="home"
class="pa-5"
>
<v-row>
<v-col cols="4">
<v-card
class="px-2 py-1"
flat
>
<v-row
class="align-center calendar"
>
<v-col
cols="1"
>
<v-icon>mdi-calendar-blank</v-icon>
</v-col>
<v-col
cols="11"
>
<vue-monthly-picker
v-model="selectedMonth"
:min="min"
:max="currentMonth"
:selectedBackgroundColor="bgColor"
:clearOption=false
dateFormat="YYYY-MM"
></vue-monthly-picker>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
<template v-if="displayUrl.length != 0">
<v-row
class="card-area"
>
<v-col
cols="4"
v-for="(items, index) in limitCount"
:key="index"
class="topCard d-flex"
>
<v-card
flat
>
<p class="pv">{{displayUrl[index].pv}} <span class="pv-text">pv</span></p>
<p class="date">{{displayUrl[index].date}}</p>
<p class="title">{{displayUrl[index].title}}</p>
<p class="writer">{{displayUrl[index].writer}}</p>
</v-card>
</v-col>
</v-row>
</template>
<template>
<v-data-table
:headers="headers"
:items="displayUrl"
:items-per-page=30
no-data-text="データがありません"
class="blog-ranking"
>
<tr
v-for="(item, index) in displayUrl"
:key="index"
>
<td>{{item.ranking[index]}}</td>
<td>{{item.date[index]}}</td>
<td>{{item.title[index]}}</td>
<td>{{item.writer[index]}}</td>
<td>{{item.pv[index]}} pv</td>
</tr>
</v-data-table>
</template>
</div>
</template>
<script>
import VueMonthlyPicker from 'vue-monthly-picker'
import moment from 'moment'
import { mapActions } from 'vuex'
export default {
name: 'home',
components: {
VueMonthlyPicker
},
data () {
return {
selectedMonth: '',
currentMonth: moment().format('YYYY-MM'),
startCurrentMonth: moment().format('YYYY-MM-01'),
endCurrentMonth: moment().format('YYYY-MM-DD'),
startCurrentSecond: moment().format('YYYY-MM-01T00:00:00'),
endCurrentSecond: moment().format('YYYY-MM-DDTHH:mm:ss'),
min: moment('2017/12'),
lastMonth: moment().endOf('month'),
perPage: 30,
article: {
items: [],
url: []
},
display: {
date: [],
title: [],
writer: [],
pv: [],
url: []
},
displayUrl: [],
headers: [
{text: '順位', sortable: false, value: 'ranking'},
{text: '投稿日', sortable: false, value: 'date'},
{text: 'タイトル', sortable: false, value: 'title'},
{text: '投稿者', sortable: false, value: 'writer'},
{text: 'PV', sortable: true, value: 'pv'},
],
bgColor: '#1d4c65'
}
},
async created () {
this.selectedMonth = moment()
await this.getBlogDetail()
await this.getData()
await this.arrayToObject()
},
watch: {
selectedMonth: async function(newDate, oldDate) {
if (!oldDate) return
this.crearDate()
if (newDate.format('YYYY-MM') == this.lastMonth.format('YYYY-MM')) {// 選択が今月だったら
this.startCurrentMonth = this.selectedMonth.startOf('month').format('YYYY-MM-DD')
this.endCurrentMonth = moment().format('YYYY-MM-DD')
this.startCurrentSecond = this.selectedMonth.startOf('month').format('YYYY-MM-DDT00:00:00')
this.endCurrentSecond = moment().format('YYYY-MM-DDTHH:mm:ss')
} else {// 選択が今月以外だったら
this.startCurrentMonth = this.selectedMonth.startOf('month').format('YYYY-MM-DD')
this.endCurrentMonth = this.selectedMonth.endOf('month').format('YYYY-MM-DD')
this.startCurrentSecond = this.selectedMonth.startOf('month').format('YYYY-MM-DDT00:00:00')
this.endCurrentSecond = this.selectedMonth.endOf('month').format('YYYY-MM-DDT23:59:59')
}
await this.getBlogDetail()
await this.getData()
await this.arrayToObject()
}
},
methods: {
...mapActions({
getBlog: 'blog/getBlog',
getGAnalytics: 'gAnalytics/getGAnalytics'
}),
crearDate () {//初期化
this.currentMonth = '',
this.startCurrentMonth = '',
this.endCurrentMonth = '',
this.startCurrentSecond = '',
this.endCurrentSecond = '',
this.article.items = [],
this.article.url = [],
this.display.date = [],
this.display.title = [],
this.display.writer = [],
this.display.pv = [],
this.display.url = [],
this.displayUrl = []
},
async getBlogDetail () {//wpから情報取得
const res = await this.getBlog({
params: {
after: this.startCurrentSecond,
before: this.endCurrentSecond,
perPage: this.perPage
}
})
const data = res.data
if (data) {
this.article.items.push(data)
} else {
alert('WordPressからの情報取得に失敗しました。')
}
},
async getData () {//GA Reporting APIから情報取得
const res = await this.getGAnalytics({
params: {
start: this.startCurrentMonth,
end: this.endCurrentMonth,
selectedMonth: this.selectedMonth.format('YYYYMM'),
conditions: this.selectedMonth.format('YYYYMM')
},
gapi: this.$gapi
})
const data = res.data
if (data) {
for (let gaCount = 0; gaCount < data.length; gaCount++) {
const gaUrl = data[gaCount].dimensions[1] //GAから取得したurl情報を順番に格納
const gaPv = data[gaCount].metrics[0].values[0] //GAから取得したpv情報を順番に格納
for (let wpCount = 0; wpCount < this.article.items[0].length; wpCount++) {//事前に取得したwpからの情報をfor文で回して順番に格納
const wp = this.article.items[0]//wpから取得した情報を一旦ここにまとめた
const wpUrl = wp[wpCount].link.replace('https://swallow-incubate.com', '')//ドメインの文字列を削除
const wpDate = wp[wpCount].date.slice(0, -9)//投稿日(お尻の時間を削除)
const wpTitle = wp[wpCount].title.rendered//ブログタイトル
const wpWriter = wp[wpCount].blog_tag[0]//投稿者
if (gaUrl == wpUrl) {//GAのURLとwpのURLが同じだったら
if(this.display.url.some(checkUrl => checkUrl == wpUrl)) {//一度格納したURLと同じものがまた回ってきたら
for (let diff = 0; diff < this.display.url.length; diff++) {
if(wpUrl == this.display.url[diff]) {
const total = parseInt(this.display.pv[diff]) + parseInt(gaPv)//文字列を整数に変換
this.display.pv[diff] = total//元のPV数に後から回ってきたPV数を追加する
}
}
break
} else {//一度も格納したことのないURLならこっち
this.display.date.push(wpDate)//投稿日を格納
this.display.title.push(wpTitle)//投稿者を格納
this.display.url.push(wpUrl)//URLを格納
this.display.pv.push(gaPv)//PV数を格納
let member = ''
switch (wpWriter) {
case ●●:
member = 'メンバー1';
this.display.writer.push(member)
break
case ■■:
member = 'メンバー2';
this.display.writer.push(member)
break
default:
member = '不明'
this.display.writer.push(member)
break
}
}
}
}
}
}
else {
alert('GAからのデータ取得に失敗しました。')
}
},
async arrayToObject () {//PVでソートし直し
for (let i = 0; i < this.display.url.length; i++) {
this.displayUrl.push({
"url": this.display.url[i],
"ranking": i+1,
"date": this.display.date[i],
"title": this.display.title[i],
"writer": this.display.writer[i],
"pv": parseInt(this.display.pv[i])
})
}
this.displayUrl.sort((a, b) => {
if (a.pv > b.pv) {
return -1
} else {
return 1
}
})
for (let i = 0; i < this.display.url.length; i++) {//ランキングの数字を再計算
this.displayUrl[i].ranking = i + 1
}
}
},
computed: {
limitCount() {//TOP3のみを表示
return this.displayUrl.slice(0, 3)
}
}
}
</script>
src/store/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue')
},
{
path: '/three-months',
name: 'threeMonths',
component: () => import('../views/ThreeMonths.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
src/store/modules/blog.js
import axios from 'axios'
export const blog = {
namespaced: true,
state: {
url: 'https://swallow-incubate.com/wp-json/wp/v2/blog?'
},
actions: {
async getBlog (state, obj) {
const ApiURL = state.state.url + 'after=' + obj.params.after + '&before=' + obj.params.before + '&per_page=' + obj.params.perPage
const result = await axios.get(ApiURL)
.then(res => {
if (res.status === 200) {
return {
result: true,
data: res.data
}
} else {
return {
result: false,
code: res.code,
data: []
}
}
})
.catch(error => {
if(error.message) {
if (Array.isArray(error.message)) {
let message = []
for (let err of error.message) {
message.push(err.msg)
}
return {
result: false,
data: message.join(',')
}
} else {
return {
result: false,
data: error.message
}
}
} else {
return {
result: false,
data: error
}
}
})
return result
}
}
}
src/store/modules/gAnalytics.js
export const gAnalytics = {
namespaced: true,
state: {
url:'https://analyticsreporting.googleapis.com/v4/reports:batchGet',
viewId: '[viewId]'
},
actions: {
async getGAnalytics ({state}, obj) {
const requestParams = {
reportRequests: [
{
viewId: state.viewId,
dateRanges: [
{
startDate: obj.params.start,
endDate: obj.params.end,
}
],
metrics: [
{
expression: 'ga:pageviews'
}
],
dimensions: [
{
name: 'ga:pageTitle'
},
{
name: 'ga:pagePath'
}
],
dimensionFilterClauses: [{
filters: [{
dimensionName: 'ga:pagePath',
expressions: [
obj.params.selectedMonth
]
},
{
dimensionName: 'ga:pagePath',
expressions: [
obj.params.conditions
]
}
]
}],
orderBys: [{
fieldName: "ga:pageviews",
sortOrder: "DESCENDING"
}]
}
]
}
const result = await obj.gapi.request({
path: state.url,
method: 'POST',
body: requestParams,
headers: {
'content-Type': 'application/json'
}
})
.then(res => {
if (res.status === 200) {
return {
result: true,
data: res.result.reports[0].data.rows
}
} else {
return {
result: false,
code: res.code,
data: []
}
}
})
.catch(err => {
console.log('gAnalytics', err)
})
return result
}
}
}
完成イメージ
cssもちゃんと設定してあげて、こんな感じになりました。

これは、2月1日〜2月29日までに公開されたブログで、かつ2月1日〜2月29日までのPV数をカウントした1ヶ月のランキング画面になっております!
1ヶ月単位で見ると、そんな大した数字ではないのですが…3ヶ月ごとになると、トップ5くらいが爆発的な数字になってるんですよね。
1位は大体あの方がさらっと取っていくのですが、2&3位争いはいつも熾烈を極めているスワブロです。
コードの説明
さてさて、垂れ流しで紹介したコードですが…ざっくり説明しますね!
src/main.js
インストールしたvue google apiをimportして、作成したGoogle APIの情報を入れます。
clientId = OAuth2.0クライアントIDから作成したクライアントID
apiKey = APIキーから作成したキー
src/App.vue
外枠を作ってあげてます。
コンポーネンツにBaseLine.vueを作っているので、そちらの読み込みをしています。
src/components/BaseLine.vue
vuetifyのナビゲーションドロワーを使っています。
ここでは、1ヶ月と3ヶ月のランキングへのメニューを作っていますが、今回は1ヶ月ランキングのみ紹介します。
src/views/Home.vue
html部分ではtop3をvuetifyのcardでレイアウトしています。
テーブルはvuetifyのData tableを使用しています(手動ソートにも対応しているため、超便利!)
script部分は細かいものはコメントで説明しましたが、ざっくり説明するとこんな感じ。
- セレクトボックスに今月を代入
- WPからデータを取得
- GA Reporting APIからデータを取得
- 上記で集計して、ソートがおかしければ再集計
ちなみに、セレクトボックスは年月のみの選択となっていますが、script内ではこんな感じになっています。
〜今月(今日が2020年4月03日11時00分00秒の場合)〜
集計開始日:2020-04-01
集計終了日:2020-04-03
集計開始日(秒まで):2020-04-01T00:00:00
集計終了日(秒まで):2020-04-03T11:00:00
〜今月以外(2月を選択した場合)〜
集計開始日:2020-02-01
集計終了日:2020-02-29
集計開始日(秒まで):2020-02-01T00:00:00
集計終了日(秒まで):2020-02-29T23:59:59
今月以外だったら1ヶ月分をごっそり集計しちゃえばいいのですが、今月の場合は今日のなうタイムまでのデータしか作成されていないので…今月以外と同じ処理でデータをgetすると「んなデータねぇーよ!この野郎!」と怒られますww
そのためにwatchのif部分は細かく設定してあげてます。
あと268行でPVでソートし直しとありますが…
ここは、恐らくやらなくても大丈夫なんです。
ただ、弊社の場合ちょっと前にサーバー移管とかなんやらやって、数ページだけGAで取得している情報が同じURLなのに別れてる…という珍事象がありまして(原因がこれかは分かりませんが…
GA上では合体しているんですが、APIから取得してくると別れてるんです。
archives/blog/xxxxxxxx/:500pv
archives/blog/xxxxxxxx/:2pv
みたいな?
中村さんから月間発表されて「たくやが1位になったのに、こっちのアプリではたくや3位なんですけどwwwあれ?分裂してるwww」っていうね。
src/store/index.js
コードがゴチャゴチャしてくるため、modules/blog.js, modules/gAnalytics.jsというモジュールを作って管理しています。
src/store/modules/blog.js
Home.vueでまとめたparams(after, before, perPage)を受け取り、それをWP Rest APIに投げて、statusが200だったらdataに格納して、Home.vueに値をぶん投げてます。
参照:WP REST API
src/store/modules/gAnalytics.js
Home.vueでまとめたparams(start, end, selectedMonth, conditions)を受け取り、reportRequestsに条件を当てはめていきGA Reporting APIに投げて、statusが200だったらdataに格納して、Home.vueに値をぶん投げてます。
まとめ
いかがでしたでしょうか?
超ローングな内容だったので、大分はしょっちゃいました😋
一応私のローカルでは動いてるんですが…だ、大丈夫かな(ひやひや
次の問題は、社内サーバーにあげようとしたらGoogle APIが「IPアドレスじゃ受け付けませんぜ!」と弾くんで、そこで頭を抱えています。
中村さんに使ってもらえなかったら、作った意味がーーー😭
ちなみに、このアプリはGAで見る権限がないと見れないのも注意!
(多分誰でも見られる方法もある気はするが…とりあえず、社内で見られればでこんな感じにしています)
無事、スワローメンバーに見てもらえる日が来ることを祈って…このブログを締めたいと思います!
ではっ!
↓↓↓ぜひチェックしてください
~提供中のヒューマンセンシング技術~
◆なりすまし判定技術
eKYC・顔認証のなりすまし対策を!
https://bio-check.pas-ta.io
◆まばたき検出技術
PCやスマホアプリに組み込めて高精度にまばたきを検出
https://blink.pas-ta.io
◆視距離推定技術
単一RGBカメラで、目からカメラまでの距離を推定
https://vd.pas-ta.io
◆虹彩認証技術
目の虹彩を利用した生体認証技術
https://iris.pas-ta.io
◆視線検出技術
アイトラッキングや次世代UIに
https://eyetrack.pas-ta.io
◆目検出技術
あらゆる目周りデータを高精度に取得
https://pupil.pas-ta.io
