PageSpeed Insights APIを利用して、新規作成したページのスコアをチェックする

最終更新日:

Table of contents

このチュートリアルでは、PageSpeed Insights API を利用して、新規作成・更新したページのスコアをチェックする方法を紹介します。処理の実行にはGitHub Actionsを利用します。

記事内で使用している主なソフトウェアのバージョン

  • newt-client-js(newt-client-js): 3.2.7
  • node-slack-sdk(@slack/web-api): 6.9.1

前提条件

  • GitHub Actionsを実行できる環境があること
  • メッセージを通知するSlackのワークスペース、チャンネルを用意していること

GitHub Actionsについて、詳細を知りたい場合はGitHubの GitHub Actions のドキュメントをご確認ください。
Slackの基本的な使い方について知りたい場合は、Slackの はじめ方 のドキュメントをご確認ください。

概要

定期的にPageSpeed Insights APIを利用してスコアを取得し、Slackに通知を行います。
ここでは、以下のように処理を行います。

  • GitHub Actions を利用して、週に1回、処理をトリガーする
  • Newtのコンテンツ情報をもとに、1週間以内に更新されたコンテンツを特定する
  • 更新されたコンテンツ情報をもとに、更新があったページのURLを特定する
  • 更新があったページについて、PageSpeed Insights APIを利用して、スコアを取得する
  • 取得したスコアを Slack に通知する

nextjs-psi1.jpg

最終的に、Slackに以下のような通知を送ります。
「モバイル」と「デスクトップ」の両方について、「パフォーマンス」「ユーザー補助」「おすすめの方法」「SEO」の4つのスコアを表示しています。

nextjs-psi2.jpg

Page Speed Insightsとは

Googleが提供する PageSpeed Insights を利用すると、ウェブページのパフォーマンス・アクセシビリティ・SEO等のスコアを測定できます。
これらの項目を定期的に確認・改善することで、ユーザー体験を向上させることができます。また、ページエクスペリエンスは、検索結果にも影響する ため、そういった意味でも継続的に確認・改善することは、価値のある取り組みと考えられます。

1. Newtのコンテンツ情報をもとに、調査対象のURLを特定する

まず、GitHub Actionsで実行するjsファイルを作成して、以下の処理を実行できるようにしましょう。

  • Newtのコンテンツ情報をもとに、1週間以内に更新されたコンテンツを特定する
  • 更新されたコンテンツ情報をもとに、更新があったページのURLを特定する

順に説明していきます。

1-1. Newtのコンテンツ情報をもとに、1週間以内に更新されたコンテンツを特定する

Newtで管理しているコンテンツのうち、1週間以内に更新されたコンテンツを特定しましょう。.github/actions/psi.js を作成します。ここでは、以下のことを行っています。

  • client を作成し、NewtのCDN APIを利用するためのクライアントを作成する
  • getArticles でNewtで管理しているコンテンツの一覧を取得する(appUidmodelUid にはご利用されているApp・モデルのUIDを指定してください)。また、このモデルでは title にコンテンツのタイトル、slug にコンテンツのスラッグが登録されているものとします
  • oneWeekBefore に実行時から1週間前の日時を指定し、最終更新の日時が1週間前より大きい投稿を取得する

※ Newt CDN APIでは、_sys.updated に最終更新の日時(最後に保存して公開した日時。保存しても公開しなかった場合は更新されない)が入るため、この日時が oneWeekBefore より大きい投稿を取得しています。gt 演算子を利用することで「より大きい」もののみを取得できます。詳細はNewtの API Reference をご確認ください。

この記事ではローカル環境で環境変数を読み込むために dotenv を利用していますが、お好きな方法で構いません
.github/actions/psi.js
1require('dotenv').config()
2const { createClient } = require('newt-client-js')
3
4const client = createClient({
5  spaceUid: process.env.NEWT_SPACE_UID + '',
6  token: process.env.NEWT_CDN_API_TOKEN + '',
7  apiType: 'cdn',
8})
9
10const getArticles = async (query) => {
11  const { items } = await client.getContents({
12    appUid: 'blog',
13    modelUid: 'article',
14    query,
15  })
16  return items
17}
18
19;(async () => {
20  try {
21    const oneWeekBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
22    const updatedArticles = await getArticles({
23      '_sys.updatedAt': { gt: oneWeekBefore.toISOString() },
24      select: ['title', 'slug'],
25    })
26
27    return
28  } catch (err) {
29    console.error(err)
30  }
31})()

この時点で、環境変数は以下のようになっています。以下を、実際の値で置き換えて定義してください。

.env
1NEWT_SPACE_UID=sample-for-docs
2NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx

1-2. 更新されたコンテンツ情報をもとに、更新があったページのURLを特定する

次に、更新があったページのURL(PageSpeed Insights APIでパフォーマンスをチェックしたいURL)を特定します。
ここでは、ブログの記事が更新されると、ブログの記事詳細ページ /articles/[slug] が更新されるとします。

サイトのドメインが https://hoge.vercel.app だとすると、取得した article から、PageSpeed Insights APIでパフォーマンスをチェックしたいURLは https://hoge.vercel.app/articles/${article.slug} とわかります。

2. PageSpeed Insights APIを利用して、スコアを取得する

2-1. PageSpeed Insights APIで利用するAPIキーを作成する

まず、APIキーの取得と使用 のドキュメントを確認して、PageSpeed Insights APIで利用するAPIキーを作成しましょう。

作成したら、そのAPIキーを環境変数 PSI_API_KEY として登録します。
以下を、実際の値で置き換えて定義してください。

.env
NEWT_SPACE_UID=sample-for-docs
NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx
PSI_API_KEY=xxxxxxx

上記のように定義しておくと、process.env.PSI_API_KEY として利用できるようになります。

2-2. PageSpeed Insights APIでパフォーマンススコアを取得する

最終的には「パフォーマンス」「ユーザー補助」「おすすめの方法」「SEO」の4つのスコアについて、「モバイル」「デスクトップ」の両方のスコアを取得しますが、まずは「デスクトップ」の「パフォーマンス」スコアから取得してみましょう。

getScores というメソッドを作成します。以下のことを行っています。

  • PageSpeed Insights APIを利用するエンドポイントとして、psiUrl を定義する。key として、先程取得したAPIキーを設定する
  • requestUrl では、測定対象となるURLを url パラメータで設定する
  • 返却されたデータから lighthouseResult の中にある categories.performance.score からパフォーマンススコアを取得する
  • console.log のところで、取得したパフォーマンススコアを表示する
.github/actions/psi.js
require('dotenv').config()
const { createClient } = require('newt-client-js')

const client = createClient({
  spaceUid: process.env.NEWT_SPACE_UID + '',
  token: process.env.NEWT_CDN_API_TOKEN + '',
  apiType: 'cdn',
})

const getArticles = async (query) => {
  const { items } = await client.getContents({
    appUid: 'blog',
    modelUid: 'article',
    query,
  })
  return items
}

const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?key=${process.env.PSI_API_KEY}`

const getScores = async (url) => {
  const requestUrl = `${psiUrl}&url=${url}`

  const res = await fetch(requestUrl)
  const data = await res.json()
  const { categories } = data.lighthouseResult
  const performanceScore = Math.round(categories.performance.score * 100)
  console.log(performanceScore)

  return
}

;(async () => {
  try {
    const oneWeekBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
    const updatedArticles = await getArticles({
      '_sys.updatedAt': { gt: oneWeekBefore.toISOString() },
      select: ['title', 'slug'],
    })
    await getScores('https://www.newt.so/docs/quick-start')

    return
  } catch (err) {
    console.error(err)
  }
})()

ここでは試しに getScores を呼び出しています(引数のurl https://www.newt.so/docs/quick-start のところには、テストで測定したいお好きなURLを設定してください)。

以下のようにnodeコマンドを実行すると、ログとしてスコア(95 など)が表示されるのを確認できます。

node .github/actions/psi

2-3. すべてのスコアを取得する

まず「パフォーマンス」「ユーザー補助」「おすすめの方法」「SEO」の4つのスコアを取得できるようにしましょう。
リクエストの パラメータ として category を設定することで、「パフォーマンス」以外のスコアも取得できます。

クエリパラメータに以下を追加することで、「パフォーマンス」「ユーザー補助」「おすすめの方法」「SEO」の4つのスコアを取得できます。

&category=performance&category=accessibility&category=best-practices&category=seo

またデフォルトでは、デスクトップのスコアのみが返却されるため、モバイル用のスコアを取得する場合は、別のリクエストを作成し strategy パラメータに mobile を設定します。

これらを踏まえて、以下のように修正します。以下のことを行っています。

  • psiUrlのクエリパラメータに category を複数設定して、「パフォーマンス」「ユーザー補助」「おすすめの方法」「SEO」の4つのスコアを取得する
  • モバイル用のリクエストとパソコン用のリクエストを別々に作成する(requestUrlForMobilerequestUrlForDesktop
  • 返却されたデータから lighthouseResult の中にある categories からそれぞれのスコアを取得する
  • console.log のところで、取得したパフォーマンススコアを表示する
.github/actions/psi.js
1require('dotenv').config()
2const { createClient } = require('newt-client-js')
3
4const client = createClient({
5  spaceUid: process.env.NEWT_SPACE_UID + '',
6  token: process.env.NEWT_CDN_API_TOKEN + '',
7  apiType: 'cdn',
8})
9
10const getArticles = async (query) => {
11  const { items } = await client.getContents({
12    appUid: 'blog',
13    modelUid: 'article',
14    query,
15  })
16  return items
17}
18
19const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?key=${process.env.PSI_API_KEY}&category=performance&category=accessibility&category=best-practices&category=seo`
20
21const getScores = async (url) => {
22  const requestUrl = `${psiUrl}&url=${url}`
23  const requestUrlForMobile = `${requestUrl}&strategy=mobile`
24  const requestUrlForDesktop = `${requestUrl}&strategy=desktop`
25
26  const resMobile = await fetch(requestUrlForMobile)
27  const dataMobile = await resMobile.json()
28  const { categories } = dataMobile.lighthouseResult
29  const performanceScoreMobile = Math.round(categories.performance.score * 100)
30  const accessibilityScoreMobile = Math.round(
31    categories.accessibility.score * 100,
32  )
33  const bestPracticesScoreMobile = Math.round(
34    categories['best-practices'].score * 100,
35  )
36  const seoScoreMobile = Math.round(categories.seo.score * 100)
37
38  const resDesktop = await fetch(requestUrlForDesktop)
39  const dataDesktop = await resDesktop.json()
40  const { categories: categoriesDesktop } = dataDesktop.lighthouseResult
41  const performanceScoreDesktop = Math.round(
42    categoriesDesktop.performance.score * 100,
43  )
44  const accessibilityScoreDesktop = Math.round(
45    categoriesDesktop.accessibility.score * 100,
46  )
47  const bestPracticesScoreDesktop = Math.round(
48    categoriesDesktop['best-practices'].score * 100,
49  )
50  const seoScoreDesktop = Math.round(categoriesDesktop.seo.score * 100)
51
52  console.log(
53    performanceScoreMobile,
54    accessibilityScoreMobile,
55    bestPracticesScoreMobile,
56    seoScoreMobile,
57  )
58  console.log(
59    performanceScoreDesktop,
60    accessibilityScoreDesktop,
61    bestPracticesScoreDesktop,
62    seoScoreDesktop,
63  )
64
65  return
66}
67
68;(async () => {
69  try {
70    const oneWeekBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
71    const updatedArticles = await getArticles({
72      '_sys.updatedAt': { gt: oneWeekBefore.toISOString() },
73      select: ['title', 'slug'],
74    })
75    await getScores('https://www.newt.so/docs/quick-start')
76
77    return
78  } catch (err) {
79    console.error(err)
80  }
81})()

試しにローカルサーバーを立ち上げて、curlコマンドを実行してみましょう(getScores から呼び出すURLはお好きなURLを設定してください)。

node .github/actions/psi

以下のように、スコアが表示されることがわかります。

90 100 100 100
98 100 100 100

上記で利用した以外のリクエスト・レスポンスを利用したい場合は、Googleの ドキュメント をご確認ください。

これで、PageSpeed Insights APIを利用して、スコアを取得できるようになりました。

3. Slack Appを作成する

続いて、取得したスコアをSlackに通知するために、Slack Appを作成します。

3-1. Slack Appを作成する

https://api.slack.com/apps より、Slack Appを作成します。
「Create New App」から「From scratch」を選択して、Appの名前を入力し、メッセージを送るワークスペースを選択します。
入力が終わったら、「Create App」を押します。

nextjs-psi3.jpg

3-2. パーミッションを設定する

「OAuth & Permissions」の「Scopes」にある「Bot Token Scopes」から「Add an OAuth Scope」をクリックします。
メッセージを投稿する chat.postMessage では chat:write のスコープが必要なため、chat:write を追加しましょう。

nextjs-psi4.jpg

3-3. ワークスペースにインストールする

続いて、「OAuth & Permissions」の「OAuth Tokens for Your Workspace」より「Install to Workspace」をクリックして、作成したSlack Appをワークスペースにインストールしましょう。

nextjs-psi5.jpg

インストールすると、Bot User OAuth Tokenが発行されます。
このTokenを環境変数 SLACK_APP_TOKEN として登録しておきましょう。
以下を、実際の値で置き換えて定義してください。

.env
NEWT_SPACE_UID=sample-for-docs
NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx
PSI_API_KEY=xxxxxxx
SLACK_APP_TOKEN=xoxb-xxxxxxxxxxxx

3-4. チャンネルにAppを追加する

次にメッセージを通知するチャンネルにAppを追加します。
チャンネル名をクリックし、Integrations を選択しましょう。「Add apps」より作成したAppを「Add」します。

nextjs-psi6.jpg

すると、以下のようにチャンネルに追加されます。

nextjs-psi7.jpg

3-5. チャンネルのIDを確認する

続いて、チャンネルIDを確認します。
Slackのアプリを利用している場合、チャンネル名をクリックし、About を選択しましょう。下部にIDが表示されます。

nextjs-psi8.jpg

WebブラウザでSlackを利用している場合、チャンネルのURLを確認します。
SlackのURLは以下のようになっているため、URLからチャンネルIDを確認しましょう。

https://app.slack.com/client/{ワークスペースID}/{チャンネルID}

例えば、URLが https://app.slack.com/client/Txxxxxx/Cyyyyyy の場合、チャンネルIDは Cyyyyyy となります。

このチャンネルIDを環境変数 SLACK_CHANNEL_ID として登録しておきましょう。
以下を、実際の値で置き換えて定義してください。

.env
NEWT_SPACE_UID=sample-for-docs
NEWT_CDN_API_TOKEN=xxxxxxxxxxxxxxx
PSI_API_KEY=xxxxxxx
SLACK_APP_TOKEN=xoxb-xxxxxxxxxxxx
SLACK_CHANNEL_ID=Cyyyyyy

4. 取得したスコアをSlackに通知する

4-1. node-slack-sdkをインストールする

node-slack-sdk をインストールします。

npm install @slack/web-api
# or
yarn add @slack/web-api

4-2. Slackに通知する

続いて、シンプルな形式で、Slackにスコアを送信できるようにしましょう。Slackの chat.postMessage を利用します。
メッセージの作成には blocks を利用します。

以下のことを行っています。

  • getScorestitle の引数を追加し、タイトル・URL・モバイルの4スコア・デスクトップの4スコアの情報を持たせたblocksを返却できるようにする
  • 更新したコンテンツがある場合、各コンテンツについて getScores を並列に実行する。その際、URLには1-3で特定したURLを指定する(ここでは https://hoge.vercel.app/articles/${article.slug}
  • chat.postMessage を利用して、メッセージを送る。オプションの詳細については chat.postMessage のドキュメントをご確認ください
.github/actions/psi.js
1require('dotenv').config()
2const { WebClient } = require('@slack/web-api')
3const { createClient } = require('newt-client-js')
4
5const client = createClient({
6  spaceUid: process.env.NEWT_SPACE_UID + '',
7  token: process.env.NEWT_CDN_API_TOKEN + '',
8  apiType: 'cdn',
9})
10
11const getArticles = async (query) => {
12  const { items } = await client.getContents({
13    appUid: 'blog',
14    modelUid: 'article',
15    query,
16  })
17  return items
18}
19
20const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?key=${process.env.PSI_API_KEY}&category=performance&category=accessibility&category=best-practices&category=seo`
21
22const getScores = async (title, url) => {
23  const requestUrl = `${psiUrl}&url=${url}`
24  const requestUrlForMobile = `${requestUrl}&strategy=mobile`
25  const requestUrlForDesktop = `${requestUrl}&strategy=desktop`
26
27  const resMobile = await fetch(requestUrlForMobile)
28  const dataMobile = await resMobile.json()
29  const { categories } = dataMobile.lighthouseResult
30  const performanceScoreMobile = Math.round(categories.performance.score * 100)
31  const accessibilityScoreMobile = Math.round(
32    categories.accessibility.score * 100,
33  )
34  const bestPracticesScoreMobile = Math.round(
35    categories['best-practices'].score * 100,
36  )
37  const seoScoreMobile = Math.round(categories.seo.score * 100)
38
39  const resDesktop = await fetch(requestUrlForDesktop)
40  const dataDesktop = await resDesktop.json()
41  const { categories: categoriesDesktop } = dataDesktop.lighthouseResult
42  const performanceScoreDesktop = Math.round(
43    categoriesDesktop.performance.score * 100,
44  )
45  const accessibilityScoreDesktop = Math.round(
46    categoriesDesktop.accessibility.score * 100,
47  )
48  const bestPracticesScoreDesktop = Math.round(
49    categoriesDesktop['best-practices'].score * 100,
50  )
51  const seoScoreDesktop = Math.round(categoriesDesktop.seo.score * 100)
52
53  const blocks = [
54    {
55      type: 'section',
56      text: {
57        type: 'mrkdwn',
58        text: `${title}\n${url}\nMobile: ${performanceScoreMobile} ${accessibilityScoreMobile} ${bestPracticesScoreMobile} ${seoScoreMobile}\nDesktop: ${performanceScoreDesktop} ${accessibilityScoreDesktop} ${bestPracticesScoreDesktop} ${seoScoreDesktop}`,
59      },
60    },
61    {
62      type: 'divider',
63    },
64  ]
65
66  return blocks
67}
68
69;(async () => {
70  try {
71    const oneWeekBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
72    const updatedArticles = await getArticles({
73      '_sys.updatedAt': { gt: oneWeekBefore.toISOString() },
74      select: ['title', 'slug'],
75    })
76
77    const blocks = []
78    if (updatedArticles.length === 0) {
79      blocks.push({
80        type: 'section',
81        text: {
82          type: 'mrkdwn',
83          text: `更新されたページはありませんでした`,
84        },
85      })
86    } else {
87      await Promise.all(
88        updatedArticles.map(async (article) => {
89          const _blocks = await getScores(
90            article.title,
91            `https://hoge.vercel.app/articles/${article.slug}`,
92          )
93          blocks.push(..._blocks)
94        }),
95      )
96    }
97
98    const token = process.env.SLACK_APP_TOKEN
99    const client = new WebClient(token)
100    const channel = process.env.SLACK_CHANNEL_ID + ''
101
102    await client.chat.postMessage({
103      channel,
104      blocks,
105      unfurl_links: false,
106    })
107
108    return
109  } catch (err) {
110    console.error(err)
111  }
112})()

これで、シンプルな形式でスコアを通知できるようになりました。
nextjs-psi9.jpg

4-3. メッセージを装飾する

最後にメッセージを装飾して、結果を見やすくしてみましょう。
以下を修正します。

  • scoreWithEmoji のメソッドを利用して、スコアの隣に絵文字をつける
  • header を追加して、メッセージの先頭にヘッダーを入れる
.github/actions/psi.js
1require('dotenv').config()
2const { WebClient } = require('@slack/web-api')
3const { createClient } = require('newt-client-js')
4
5const client = createClient({
6  spaceUid: process.env.NEWT_SPACE_UID + '',
7  token: process.env.NEWT_CDN_API_TOKEN + '',
8  apiType: 'cdn',
9})
10
11const getArticles = async (query) => {
12  const { items } = await client.getContents({
13    appUid: 'blog',
14    modelUid: 'article',
15    query,
16  })
17  return items
18}
19
20const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?key=${process.env.PSI_API_KEY}&category=performance&category=accessibility&category=best-practices&category=seo`
21
22const scoreWithEmoji = (score) => {
23  if (score >= 90) {
24    return `:white_check_mark:${score}`
25  } else if (score >= 70) {
26    return `:warning:${score}`
27  } else if (score >= 50) {
28    return `:rotating_light:${score}`
29  } else {
30    return `:scream:${score}`
31  }
32}
33
34const getScores = async (title, url) => {
35  const requestUrl = `${psiUrl}&url=${url}`
36  const requestUrlForMobile = `${requestUrl}&strategy=mobile`
37  const requestUrlForDesktop = `${requestUrl}&strategy=desktop`
38
39  const resMobile = await fetch(requestUrlForMobile)
40  const dataMobile = await resMobile.json()
41  const { categories } = dataMobile.lighthouseResult
42  const performanceScoreMobile = Math.round(categories.performance.score * 100)
43  const accessibilityScoreMobile = Math.round(
44    categories.accessibility.score * 100,
45  )
46  const bestPracticesScoreMobile = Math.round(
47    categories['best-practices'].score * 100,
48  )
49  const seoScoreMobile = Math.round(categories.seo.score * 100)
50
51  const resDesktop = await fetch(requestUrlForDesktop)
52  const dataDesktop = await resDesktop.json()
53  const { categories: categoriesDesktop } = dataDesktop.lighthouseResult
54  const performanceScoreDesktop = Math.round(
55    categoriesDesktop.performance.score * 100,
56  )
57  const accessibilityScoreDesktop = Math.round(
58    categoriesDesktop.accessibility.score * 100,
59  )
60  const bestPracticesScoreDesktop = Math.round(
61    categoriesDesktop['best-practices'].score * 100,
62  )
63  const seoScoreDesktop = Math.round(categoriesDesktop.seo.score * 100)
64
65  const blocks = [
66    {
67      type: 'section',
68      text: {
69        type: 'mrkdwn',
70        text: `${title}\n${url}`,
71      },
72    },
73    {
74      type: 'section',
75      fields: [
76        {
77          type: 'mrkdwn',
78          text: `*Mobile*\n${scoreWithEmoji(
79            performanceScoreMobile,
80          )}(パフォーマンス)\n${scoreWithEmoji(
81            accessibilityScoreMobile,
82          )}(ユーザー補助)\n${scoreWithEmoji(
83            bestPracticesScoreMobile,
84          )}(おすすめの方法)\n${scoreWithEmoji(seoScoreMobile)}(SEO)`,
85        },
86        {
87          type: 'mrkdwn',
88          text: `*Desktop*\n${scoreWithEmoji(
89            performanceScoreDesktop,
90          )}(パフォーマンス)\n${scoreWithEmoji(
91            accessibilityScoreDesktop,
92          )}(ユーザー補助)\n${scoreWithEmoji(
93            bestPracticesScoreDesktop,
94          )}(おすすめの方法)\n${scoreWithEmoji(seoScoreDesktop)}(SEO)`,
95        },
96      ],
97    },
98    {
99      type: 'divider',
100    },
101  ]
102
103  return blocks
104}
105
106;(async () => {
107  try {
108    const oneWeekBefore = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
109    const updatedArticles = await getArticles({
110      '_sys.updatedAt': { gt: oneWeekBefore.toISOString() },
111      select: ['title', 'slug'],
112    })
113    const updatedArticles = await getArticles()
114
115    const blocks = []
116    if (updatedArticles.length === 0) {
117      blocks.push({
118        type: 'section',
119        text: {
120          type: 'mrkdwn',
121          text: `更新されたページはありませんでした`,
122        },
123      })
124    } else {
125      await Promise.all(
126        updatedArticles.map(async (article) => {
127          const _blocks = await getScores(
128            article.title,
129            `https://hoge.vercel.app/articles/${article.slug}`,
130          )
131          blocks.push(..._blocks)
132        }),
133      )
134    }
135
136    const token = process.env.SLACK_APP_TOKEN
137    const client = new WebClient(token)
138    const channel = process.env.SLACK_CHANNEL_ID + ''
139    const header = [
140      {
141        type: 'section',
142        text: {
143          type: 'mrkdwn',
144          text: '今週のPSIレポートです:pencil:\n:scream:0 - 49\n:rotating_light:50 - 69\n:warning:70 - 89\n:white_check_mark:90 - 100',
145        },
146      },
147      {
148        type: 'divider',
149      },
150      {
151        type: 'section',
152        text: {
153          type: 'mrkdwn',
154          text: `*今週更新された記事*`,
155        },
156      },
157    ]
158
159    await client.chat.postMessage({
160      channel,
161      blocks: [...header, ...blocks],
162      unfurl_links: false,
163    })
164
165    return
166  } catch (err) {
167    console.error(err)
168  }
169})()

これで、以下のような形式でSlackに通知を送れるようになりました。

nextjs-psi2.jpg

5. GitHub Actionsを設定する

最後にGitHub Actionsを設定し、定期実行が行われるようにします。

5-1. シークレットの定義

まず、ワークフローで利用するシークレットを定義します。
「Settings」>「Secrets and variables」から「Actions」を選択して、「New repository secret」をクリックします。
NEWT_SPACE_UIDNEWT_CDN_API_TOKENPSI_API_KEYSLACK_APP_TOKENSLACK_CHANNEL_ID を追加しましょう。
値を入力して「Add secret」をクリックします。
これでワークフローから上記のシークレットを呼び出せるようになりました。

nextjs-psi10.jpg

詳細はGitHubの GitHub Actions でのシークレットの使用 のドキュメントをご確認ください。

5-2. ワークフローの定義

.github/workflows/psi.yml を作成し、ワークフローを定義します。以下のことを行っています。

  • schedule を利用して、定期実行のスケジュールを定義する。日本時間ではなく、UTCなので注意する。ここでは毎週月曜日の9時(日本時間)に実行するよう 0 0 * * 1 で定義している
  • あわせて手動実行も行えるように workflow_dispatch も指定する
  • 登録したシークレットを環境変数として設定する
.github/workflows/psi.yml
1name: psi
2on:
3  schedule:
4    - cron: '0 0 * * 1'
5  workflow_dispatch:
6jobs:
7  check_psi:
8    runs-on: ubuntu-latest
9    timeout-minutes: 10
10    steps:
11      - uses: actions/checkout@v4
12      - uses: actions/setup-node@v3
13        with:
14          node-version: '18'
15          cache: 'yarn'
16      - run: yarn
17      - name: Check PSI
18        env:
19          NEWT_SPACE_UID: ${{secrets.NEWT_SPACE_UID}}
20          NEWT_CDN_API_TOKEN: ${{secrets.NEWT_CDN_API_TOKEN}}
21          PSI_API_KEY: ${{secrets.PSI_API_KEY}}
22          SLACK_APP_TOKEN: ${{secrets.SLACK_APP_TOKEN}}
23          SLACK_CHANNEL_ID: ${{secrets.SLACK_CHANNEL_ID}}
24        run: node .github/actions/psi

以上で、1週間以内に更新されたURLを対象に、PSIスコアを通知できるようになりました。
以下のようなアレンジも可能ですので、ぜひお試しください。

  • ページの更新時に Webhook を利用してPSIスコアを通知する
  • 月に1回、主要なページのPSIスコアを通知する
NewtMade in Newt