2021年10月14日

Google SpreadsheetとGoogle Slidesでお手軽動的OGP with Nuxt

読了時間の目安: 9分


いま話題のスペースに出会えるspaces.bz(クローズ済み)では、デイリーランキングで動的OGPを実装しています。

実はこれ、Google SpreadsheetとGoogle Slidesを使って、GASで毎日OGP画像を生成して表示させているんです。 結構面白いアイデアなのでシェアします😊。

全体像

まずこの記事で紹介する全体像を示します。

  1. 動的OGPのもとになるSlidesを作成
  2. 動的OGPのデータのもとになるSpreadsheetを作成
  3. GASを作成
    • SpreadsheetのデータをもとにSlidesの文字列を置換
    • Slidesを画像ファイルでエクスポートしてDriveに保存
    • Driveに保存した画像ファイルのIDをSpreadsheetに追加
    • Slidesを元の文字列に置換し直す
  4. SpreadsheetをWebApp(API)で公開
  5. Nuxtアプリで動的OGPを設定

ちょっと長いですが、お付き合いいただけると嬉しいです🙏。

1. 動的OGPの元になるSlidesを準備

まずはいい感じのOGPのデザインをGoogle Slidesで作ります。

slidesのイメージ

Coolです。ポイントとしてはOGPは1200mmx628mmのサイズがよいので、「ファイル > ページ設定」からスライドの縦横サイズを変更してます。

このスライドに書いている「name」はこの後置換するためのキーワードになってます。

2. 動的OGPのデータのもとになるSpreadsheetを準備

「name」を置換していきたいので、そんな感じのSpreadsheetを用意します。 spaces.bzの場合は、その日のランキング10位までをGASで集計して、そのデータを元に置換しています。 今回は簡単な例なので、次のようなシートを用意します。

idnameogpId
1Test A
2Test B
3Test C

ogp_idはいまのところ空ですが列だけ用意しておきます。後でGASでOGP画像のファイルを生成したときに画像ファイルのIDをセットするカラムです。

3. GASを作成

この記事の佳境です。

まずコードの全容をさらします。 説明しやすいように書いていますが、内容を理解いただければ色々な書き方があります。

コード.js
function main() {
const ss = SpreadsheetApp.openById('<SpreadsheetのID>')
const sheet = ss.getSheetByName('<Sheetの名前>')
const data = sheet.getDataRange().getValues()
const columnNames = data.shift()
const presentationId = '<SlidesのID>'
const folder = DriveApp.getFolderById('<FolderのID>')
data.forEach((row, index) => {
const name = row[1]
replaceText(presentationId, name)
const ogpId = downloadImage(presentationId, `${index}.png`, folder)
row[2] = ogpId
resetText(presentationId, name)
})
data.unshift(columnNames)
sheet.getRange(1, 1, data.length, data[0].length).setValues(data)
}
function replaceText(presentationId, name) {
const presentation = SlidesApp.openById(presentationId)
const slide = presentation.getSlides()[0]
slide.replaceAllText('name', name)
presentation.saveAndClose()
}
function downloadImage(presentationId, fileName, folder) {
const presentation = SlidesApp.openById(presentationId)
const slide = presentation.getSlides()[0]
const slideId = slide.getObjectId()
const url = `https://docs.google.com/presentation/d/${presentationId}/export/png?id=${presentationId}&pageId=${slideId}`
const options = {
headers: {
Authorization: `Bearer ${ScriptApp.getOAuthToken()}`
}
}
const response = UrlFetchApp.fetch(url, options)
const image = response.getAs(MimeType.PNG)
image.setName(fileName)
const file = folder.createFile(image)
return file.getId()
}
function resetText(presentationId, name) {
const presentation = SlidesApp.openById(presentationId)
const slide = presentation.getSlides()[0]
slide.replaceAllText(name, 'name')
presentation.saveAndClose()
}

少し長いですが、少しずつ区切ってみていきます。

3-1. 各種変数の設定

const ss = SpreadsheetApp.openById('<SpreadsheetのID>')
const sheet = ss.getSheetByName('<Sheetの名前>')
const data = sheet.getDataRange().getValues()
const columnNames = data.shift()
const presentationId = '<SlidesのID>'
const folder = DriveApp.getFolderById('<FolderのID>')

最初のconstたちは変数の設定です。

変数説明
ss2で作成したSpreadsheet。<SpreadsheetのID>https://docs.google.com/spreadsheets/d/<この部分>/edit
sheet2で作成したシート
datasheetのデータをArrayで取得したもの
columnNamesdataの1行目を取り出したもの
presentationId1で作成したSlidesのID。https://docs.google.com/presentation/d/<この部分>/edit#slide=id.p
folder作成したOGP画像を格納しておきたいGoogle Driveのフォルダ。<FolderのID>https://drive.google.com/drive/u/0/folders/<この部分>。このフォルダは公開設定にしておきます(後述)

このとき、datacolumnNamesは次のようになっています。

data = [
['1', 'Test A', ''],
['2', 'Test B', ''],
['3', 'Test C', '']
]
columnNames = ['id', 'name', 'ogpId']

3-2. 置換して画像保存して置換し直す

data.forEach((row, index) => {
const name = row[1]
replaceText(presentationId, name)
const ogpId = downloadImage(presentationId, `${index}.png`, folder)
row[2] = ogpId
resetText(presentationId, name)
})

ここが核です。なにか色々やっているようですが、ほとんどの行は別に作成した関数を呼び出しているのでそこも説明していきます。

まず、全体はdata.forEachで回しています。 最初にrow[1]で、Test A, Test B, Test Cnameに格納しています。 それに続いて、次の処理を行います。

  1. スライドのテキストをnameに置換(replaceText()
  2. スライドをイメージ保存(downloadImage()
  3. 保存したファイルのIDをdataに追加(row[2] = ogpId
  4. スライドのテキストを置換し直す(resetText()

3-2-1. スライドのテキストを置換

スライドのテキストの置換処理を行うreplaceText()関数を定義しました。

function replaceText(presentationId, name) {
const presentation = SlidesApp.openById(presentationId)
const slide = presentation.getSlides()[0]
slide.replaceAllText('name', name)
presentation.saveAndClose()
}

引数はpresentationIdと、置換後の文字列nameです。 まず、SlidesApp.openById(presentationId)で置換したいスライドがあるSlidesを開きます。 そして、getSlides()[0]を使って、そのSlidesの1枚目のスライドを取得します。 そのslideに対してreplaceAllText()を実行することで、slideの中の'name'の文字列を引数のnameに置換しています。 最後にpresentation.saveAndClose()で置換を確定しています。saveAndClose()を行わないと、次のイメージ保存で置換前の状態で保存されてしまうので要チェックです。

置換の処理はこれだけです。 置換したい文字列が複数ある場合でも同じやり方でslide.replaceAllText()を追加すればできます。 複数のスライドに対してはpresentationに対してreplaceAllText()したり、getSlides()の結果をforEachで回してスライド1枚ずつに処理することで実現できます。

3-2-2. スライドを保存

これで動的OGPのイメージの準備ができたので、これをイメージファイルに保存します。今回はpngで。

function downloadImage(presentationId, fileName, folder) {
const presentation = SlidesApp.openById(presentationId)
const slide = presentation.getSlides()[0]
const slideId = slide.getObjectId()
const url = `https://docs.google.com/presentation/d/${presentationId}/export/png?id=${presentationId}&pageId=${slideId}`
const options = {
headers: {
Authorization: `Bearer ${ScriptApp.getOAuthToken()}`
}
}
const response = UrlFetchApp.fetch(url, options)
const image = response.getAs(MimeType.PNG)
image.setName(fileName)
const file = folder.createFile(image)
return file.getId()
}

何をするかというと、SlidesをダウンロードするURLにリクエストを投げて、レスポンスをpngファイルにして保存しようというやり方です。

引数は、presentationIdfolder、そしてファイル名としてfileNameを取ります。 最初のpresentationslideの式はreplaceText()と同じなので説明は省略します。 slideIdはそのスライドのIDで、https://docs.google.com/presentation/d/<presentationId>/edit#slide=id.<'この部分'>です。 これはslide.getObjectId()で取得ができます。

これらの変数を使ってリクエストURLurlを作ります。 リクエストには認証が必要なので、ScriptApp.getOAuthToken()を使ってBearer認証できるようにoptionsを作っておきます。

このurloptionsを使って、UrlFetchApp.fetch()でリクエストを投げ、レスポンスをresponseに格納しています。 responseHTTPResponseClassなので、画像ファイルとして扱うためにgetAs(MimeType.PNG)でpngファイルに変換し、setName()でファイル名を設定します。 それを、folder.createFile()folderにファイルとして保存しているって流れです。 最後に、保存したファイルfileのIDをgetId()で取得して、返り値にしています。

3-2-3. 保存したファイルのIDをデータに追加

const ogpId = downloadImage(presentationId, `${index}.png`, folder)
row[2] = ogpId

先程説明したようにdownloadImage()は作成したpngファイルのIDをreturnしてます。 それをrow[2]、つまりogpIdのカラムに設定しています。 詳しくは後ほど紹介しますが、これがNuxtアプリでGoogle Driveの画像ファイルをOGPに設定する肝だったりします。

3-2-4. スライドのテキストを置換し直す

ここまで終わったら次のループのためにスライドの文字列を元の'name'に戻しておきます。このためにresetText()の関数を用意して呼び出しています。

function resetText(presentationId, name) {
const presentation = SlidesApp.openById(presentationId)
const slide = presentation.getSlides()[0]
slide.replaceAllText(name, 'name')
presentation.saveAndClose()
}

やっていることはreplaceText()の逆なので説明は省略します。

3-3. スプレッドシートのogpIdを更新

data.unshift(columnNames)
sheet.getRange(1, 1, data.length, data[0].length).setValues(data)

最後にSpreadsheetのogpIdカラムを更新しましょう。 すでにここまでの処理でdataの各Array要素の3つ目の要素にogpIdが格納されているので、Spreadsheetを上書きすればOKですね。

ということで、data.unshift(columnNames)dataの1つ目の要素に列の名前を戻して、 sheet.getRange().setValues()でデータを上書きしています。

ここまでで画像を作成するステップが完了です。😊 ここからはSpreadsheetをもとにNuxtアプリでOGP画像を動的に設定してみましょう。

4. SpreadsheetをWebApp(API)で公開

次は先程OGPのIDを追記したSpreadsheetをWebAppで公開していきます。 このやり方は、以前に記事を書いたので詳細はそちらを見てください。

NuxtでGoogle SpreadsheetをDB代わりに使うぞ大作戦 | at-blog

GASのコードは次のようになります。先程のコード.jsに追記していきましょう。

コード.js
...
function doGet() {
const users = getUsers()
return ContentService
.createTextOutput(JSON.stringify(users))
.setMimeType(ContentService.MimeType.JSON)
}
function getUsers() {
const ss = SpreadsheetApp.openById('<SpreadsheetのID>')
const sheet = ss.getSheetByName('<シート名>')
const table = sheet.getDataRange().getValues()
const keys = table.shift()
const users = table.map((row) => {
const object = {}
row.map((value, index) => {
object[String(keys[index])] = String(value)
})
return object
})
return users
}

これを、デプロイしてWebApp公開しましょー。

5. Nuxtアプリで動的OGPを設定

まずは、Nuxtアプリで先程公開したWebAppにリクエストする下準備をします。今回はaxiosを使うことを前提として、serverMiddleware経由でWebAppを呼び出します。 こちらも過去に記事を書いていますので、詳細についてはそちらを参照してください。

NuxtでスプレッドシートをDB代わりに使うぞ大作戦 Part3 - asyncDataでだって使いたい! | at-blog

次の更新、もしくは新規作成します。

nuxt.config.js
export default {
...
axios: {
proxy: true,
},
serverMiddleware: [
'@api/'
],
...
}
api/index.js
const express = require('express')
const axios = require('axios')
const app = express()
app.get('/', async (req, res) => {
const response = axios.get('<WebAppのURL>')
res.send(response.data)
})
module.exports = {
path: '/api/',
handler: app,
}

これで下準備が完了です。あとはpagesのファイルでasyncData()を使い情報取得して、head()で画像のパスを指定してあげるだけです。

今回はpages/index.vueでクエリパラメーターidに応じてOGP画像を出し入れしましょう。パスパラメーターとかでもやり方は基本的に変わりないです。

pages/index.vue
...
<script>
export default {
async asyncData({ $axios, query }) {
const users = await $axios.$get('/api')
const user = query.id ? users.find((user) => user.id === query.id) : null
const ogpUrl = user ? `https://drive.google.com/uc?export=view&id=${user.ogpId}` : <共通のOGPイメージのパス>
return {
users: users,
ogpUrl: ogpUrl
}
},
head() {
return {
meta: [
{
hid: 'og:image',
property: 'og:image',
content: this.ogpUrl
}
]
}
}
}
</script>
...

例えばこんな感じです(nuxt.config.jsで他のタグは設定されている前提です)。 asyncDataでクエリパラメーターidが存在する場合は、usersから同じiduserを探してます。 見つかったらogpUrlhttps://drive.google.com/uc?export=view&id=${user.ogpId}を設定します。 存在しない場合は<共通のOGPイメージのパス>をセットし、head()og:imagepropertyとしてogpUrlを設定しています。 こうすることでクエリパラメーターidに応じて動的にOGPを設定できます。

さて、ここで出てきたhttps://drive.google.com/uc?export=view&id=です。 Google Drive上で画像ファイルを見ようとするとプレビューモードで表示されるでしょう。この状態ではコードからすれば画像ファイルとして扱うことができません。 実はhttps://drive.google.com/uc?export=view&id=<表示したい画像ファイルのID>であれば、プレビューモードではなく画像ファイルとして認識させることができます。 また、これで表示できるのは閲覧権限をもつユーザーのみですので、OGP画像を保存しているフォルダはすべてのユーザーに閲覧権限で公開されている必要があります。

ngrok で公開し、 Twitter Card Validator でOGPが正しく設定されているか確認してみましょう。

id=1のとき

id=1のとき

id=2のとき

id=2のとき

動的にOGP画像が表示されてることを確認できました。

まとめ

長くなってしまいましたが、これでSpreadsheetとSlidesを使ってOGP画像を生成しNuxtアプリで動的OGPを実現する一連の流れを紹介させていただきました。 かなり色々なサイトを参考にさせていただいてここまでできたので、この場を借りてお礼を。m(__)m GAS、かなり色々なことができるので楽しいですね。

参考

気に入っていただけたら、サポートもお待ちしております!
  • 名前:asato
  • 仕事:スクラムマスター
  • 好き:家族、温泉、旅行、謎解き
  • 苦手:はじめまして、あんこ、うなぎ