@memo

ゆるくインプット、ゆるくアウトプット

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

こんにちは。asatoです。

いま面白いスペースに出会えるspaces.bzでは、デイリーランキングで動的OGPを実装しています。

こちらのページからスペースをシェアすると画像が表示されます。

Twitterアカウントでは毎日ランキングを発表しているので、こちらでも確認できます〜。

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

全体像

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

  1. 動的OGPのもとになるSlidesを作成
  2. 動的OGPのデータのもとになるSpreadsheetを作成
  3. GASを作成
    • SpreadsheetのデータをもとにSlidesの文字列を置換
    • Slidesを画像ファイルでエクスポートしてDriveに保存
    • SpreadsheetにDriveに保存した画像ファイルのIDを追加
    • 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で集計して、そのデータを元に置換を行っています。 今回は簡単な例なので、以下のようなシートを用意します。

id name ogpId
1 Test A
2 Test B
3 Test C

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

3. GASを作成

この記事の佳境です!

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

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たちは変数の設定です。

変数 説明
ss 2で作成したSpreadsheet。<SpreadsheetのID>はhttps://docs.google.com/spreadsheets/d/<この部分>/edit
sheet 2で作成したシート
data sheetのデータをArrayで取得したもの
columnNames dataの1行目を取り出したもの
presentationId 1で作成した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 Cをループごとにnameに格納しています。 それに続いて、

  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()で取得ができます。

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

このurloptionsを使って、UrlFetchApp.fetch()でリクエストを投げ、レスポンスをresponseに格納しています。 responseはHTTPResponseというClassになっているので、画像ファイルとして扱えるように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で公開していきます。 このやり方は、以前に記事を書いたので詳細はそちらを見てください!

GASのコードは以下のようになります。先程のコード.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.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のファイルでfetch()で情報取得して、head()で画像のパスを指定してあげるだけです。

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

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

例えばこんな感じ。(nuxt.config.jsで他のタグは設定されている前提です!) fetch内でクエリパラメーターidが存在する場合は、usersから同じidのuserを探してます。 見つかったら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、かなり色々なことができるので楽しいですね。

参考