Nuxt.js + FirebaseのSPAで動的にOGPを生成することのあれこれ


こんにちは、[カノイ](https://twitter.com/kanoi_y)です。
フリーでweb制作をしたり、個人でwebサービスを開発したりしています。

先日、[パラメーカー](https://paramaker.net)という「オリジナルのパラメーターを簡単に作成できるサービス」をリリースしました（ぜひ遊んでみてください！！！！）。
![paramaker_ogp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/686970/177949fe-d4e8-8b0c-766c-15b9bff337f4.png)


このサービスを開発してる中でOGPの生成でめっちゃ詰まったので、その辺りのことをシェアします。

##　Nuxt.jsでのOGP設定について

OGPとは、「Open Graph Protcol」の略でFacebookやTwitterなどのSNSでシェアした際に、OGPを設定したWEBページのタイトルやイメージ画像、概要などを正しく伝えるための仕組みのことです。

nuxt.jsでは、サイト全体のOGPをnuxt.config.jsで設定します。こんな感じです（簡略化しています）。

```js:nuxt.config.js
      head: {
    htmlAttrs: {
      lang: "ja",
      prefix: "og: http://ogp.me/ns#"
    },
    meta: [
      { charset: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      {
        hid: "og:site_name",
        property: "og:site_name",
        content: "サイトの名前"
      },
      { hid: "og:type", property: "og:type", content: "website" },
      { hid: "og:url", property: "og:url", content: "サイトのURL" },
      {
        hid: "og:title",
        property: "og:title",
        content: "サイトのタイトル"
      },
      {
        hid: "og:description",
        property: "og:description",
        content:
          "サイトのディスクリプション"
      },
      {
        hid: "og:image",
        property: "og:image",
        content: "iイメージ画像のURL"
      }
    ]
  },
```

上記の方法だと、サイトのどのページであっても同じOGPになってしまいます。個々のページに個別にOGPを設定したい場合は、個々のページのスクリプトタグ内に同じように書くことで設定することが出来ます。

下のコードのように変数を入れて書くことで、動的にOGPを設定することもできるのですが、、、

```
        {
          hid: "og:image",
          property: "og:image",
          content: `${this.ogpImage}`
        }

```

しかし、このままではSPAではうまくいきません。ちくしょーーーー！！！！

### 　なんで上手くいかないの？

理由は単純！FacebookやTwitterなどのSNSのクローラーはクロールの際に**JavaScriptを実行しない**からです。クライアント側でJavaScriptによるOGPの変更をしても、SNSでは反映されません。

SSRだと、サーバサイドでレンダリングしてHTMLを返すのでうまくいくのですが、、、、OGPのためにSSRを選択するのは中々辛いです。

##　SPAで動的なOGPを生成するには？

色々な方法があると思いますが、僕が試したのはこの二つです。

* Prerendering(Netlify)
* FirebaseのFunctions

###   Prerendering

[Netlify](https://www.netlify.com/)というとても有名な静的サイトのホスティングサービスがあるのですが、なんとこのサービスを使えば簡単に動的なOGPを生成することが出来るようです。詳しくは下のリンクを参照してください。

▼ 参考
[Prerendering | Netlify Docs](https://docs.netlify.com/site-deploys/post-processing/prerendering/)
[NetlifyのPrerendering機能を使ってクライアントサイドのみの実装でOGP対応する](https://blog.microcms.io/ogp-with-client-side-javascript/)

しかし、僕の場合はうまくいきませんでした。うまくいかなかった原因はよく分かっていないのですが、[ここ](https://community.netlify.com/t/support-guide-understanding-and-debugging-prerendering/150)に書かれているようにmetaタグの変更が遅くてその前の状態で設定されてしまったのではないかと思っています（ただ、window.prerenderReadyの設定もしてそれでもできなかったので、これが原因ではないかも。詳しい方がいたら教えてください～）。

NetlifyのPrerenderingはまだbata版で情報がまだそんなに多くないこともあり、今回はPrerenderingを使用するのは断念しました。

###  FirebaseのFunctions

この方法は割と使われているようで、この方法を解説している記事はたくさんありました。

ざっくりどのような流れでOGPを生成するかというと、

1. 動的にOGP生成させたいURLにアクセスがあったら、FirebaseHostingのrewrite設定によって、Functionsを呼び出す。

2. FunctionsでURLを元に動的に生成したOGPタグを組み込んだHTMLを出力。bodyにはスクリプトタグの中にリダイレクト処理を記述。

3. FacebookやTwitterなどのSNSのクローラーはJavaScriptを実行しないので、2のOGPタグを読み取るだけで終了。

4. サイトを見るユーザーは、元のURLからリダイレクト先に飛ぶ。

という感じです。

すでに詳しく解説されている記事がたくさんあるので、基本的なことは下の参考リンクを参照してください。

▼ 参考

[SNS映えするWebアプリを...！FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった](https://qiita.com/yuneco/items/5e526464939082862f5d#%E5%BB%B6%E9%95%B7%E6%88%A6%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%81%AA%E3%81%97%E3%81%A7%E5%8B%95%E7%9A%84%E3%81%ABogp%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)


今回の開発でもほとんど同じ構成でしたが、違う部分もあるのでそこをまとめます。


## Firebase Functionsの注意点

### 1、二重のリダイレクト

URLにアクセスしたユーザーはリダイレクト処理により違うURLに飛ばされるのですが、記事の方法では、VueのRouterを使ってまた元のURLに戻します。

この方法でも良いのですが、僕はリダイレクトを出来れば一回にしたいと思ったので、元のURLに戻さずにそのまま使えるようにしました。

[パラメーカー](https://paramaker.net/)では動的にOGPを生成したいページを`/after/:id` と して、同じコンテンツが`/aftercreate/?id=xxxxx`で表示されるようにしています。`/after/:id` にアクセスがあったら、リダイレクト処理で`/aftercreate/?id=xxxxx`に飛ばし、ユーザーは同じコンテンツを見ることが出来ます。

注意点としては、Functionsを実行するページは、**動的なルーティング**である必要があります。その理由は、Firebase Hostingにはレスポンスの優先順位が決まっているらしく、以下のようになっています。

> /__/* パスセグメントで始まる予約済みの名前空間
リダイレクトの構成
正確に一致する静的コンテンツ
リライトの構成
カスタムの 404 ページ
デフォルトの 404 ページ


引用元：[ホスティング動作を構成する | Firebase](https://firebase.google.com/docs/hosting/full-config?hl=ja#section-priorities)

Functionsはリライトで呼び出されるのですが、それよりも静的コンテンツのほうが優先度が高いので、静的なページがすでにある場合はFunctionsは呼び出されずにそのページが返されてしまいます。なので、Functionsを実行するページは動的なルーティングである必要があります。

### 2、　FirebaseのCloud Storageを使用する場合

[パラメーカー](https://paramaker.net/)では、FirebaseのStorageからダウンロードしてOGP画像に設定しています。Functions側でStorageを使用している記事があまり見つからなかったので、その時の注意点をまとめます。

気を付けないといけないのは、クライアント側でのStorageの使い方とFunctions側（サーバーサイド）での使い方は異なるということです。それは、Functions側では通常firebase-adminを利用するのですが、その場合はGoogle Cloud StorageのAPIが使われるようで、仕様が異なるらしいです。

Functions側のStorageの扱いは、[こちらの記事](https://qiita.com/kira_puka/items/8743c2b1a012a934ef92)がとても分かりやすかったので、がっつり引用させていただきます。以下、引用。

```

const admin = require("firebase-admin");

// 配置したサービスアカウントの秘密鍵を取得
const serviceAccount = require("./key/XXXXX.json");

// firebase-adminを初期化
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  // storageBucketはローカルで実行するときのみ必要。
  // Functions上ではFunctions側で設定される
  storageBucket: "your-storage-bucket-name.appspot.com"
});

// storageのbucketのインスタンスを取得
const bucket = admin.storage().bucket();

/**
 * Cloud Storageへのアップロード
 *
 * @param {string} uploadFile アップロードするファイル
 * @param {string} uploadPath アップロード先のStorageのパス
 */
async function upload(uploadFile, uploadPath) {
  await bucket.upload(uploadFile, { destination: uploadPath });
}

/**
 * Cloud Storageへのダウンロード(StoregeのURL取得)
 * 簡易化のため、署名付きURLの取得ではなく、Storage上のURLを生成しています。
 * 
 * @param {string} uploadedPath アップロード済みのstorage上のパス
 * @returns {string} ダウンロードURL
 */
async function download(uploadedPath) {
  const STORAGE_ROOT = "https://firebasestorage.googleapis.com/v0/b";
  const bucketName = bucket.name;
  const dlPath = encodeURIComponent(uploadedPath);
  const dlURL = `${STORAGE_ROOT}/${bucketName}/o/${dlPath}?alt=media`;

  return dlURL;
}

/**
 * Cloud Storage上のファイルの存在確認
 *
 * @param {string} uploadedPath アップロード済みのstorage上のパス
 * @returns {boolean} ファイルの存在有無
 */
async function exists(uploadedPath) {
  const exists = await bucket.file(uploadedPath).exists();
  return exists[0];
}

/**
 * Cloud Storage上のファイルの削除
 *
 * @param {string} uploadedPath アップロード済みのstorage上のパス
 */
async function deleteFile(uploadedPath) {
  await bucket.file(uploadedPath).delete();
}

// ****************************
// * MAIN
// ****************************
async function main() {
  console.log(`***** START MAIN`);

  // アップロード
  const uploadFile = "./ogp_top.png"; // アップロードしたいファイルのパス
  const uploadPath = "ogp/generate_ogp.png"; // アップロード先のStorage上のパス
  await upload(uploadFile, uploadPath);
  console.info(`${uploadFile} is uploaded into '${uploadPath}'.`);

  // ダウンロード
  const uploadedPath = uploadPath;
  const downloadURL = await download(uploadedPath);
  console.info(`downloadURL=${downloadURL}`);

  // ファイルの存在確認
  const isExists = await exists(uploadedPath);
  console.info(`isExists=${isExists}`);

  // ファイルの削除
  await deleteFile(uploadedPath);
  console.info(`${uploadedPath} is deleted.`);

  console.log(`***** END   MAIN`);
}

main().then();

```

注意点としては、storageを利用するのにサービスアカウントなるものが必要なことです。

サービスアカウントは、Firebaseコンソールから設定できます。

1. 画面左上の「歯車マーク」をクリック
2. 「プロジェクトの設定」をクリック
3. 「サービスアカウント」タブをクリック
4. 「新しい秘密鍵の生成」ボタンをクリック

これでサービスアカウントの秘密鍵のJSONファイルをダウンロードすることが出来ますので、このJSONをfunctionsディレクトリ内に配置します（上のコードでは、functionsディレクトリ内のkeyディレクトリに配置しています）。以上を行うことで、Functions側でStorageを利用することが出来るようになります。


## 終わりに

Nuxt.js + FirebaseのSPAで動的にOGPを生成することに関してのあれこれをを書いてきました。引用だらけで自分のメモのようになってしまいましたが（それはそれでいいんですが）、なにかお役に立っていればうれしいです！！

間違っているところがあれば、コメントを書いていただけるとありがたいです！

最後に、宣伝！！

[パラメーカー](https://paramaker.net)という「オリジナルのパラメーターを簡単に作成できるサービス」をリリースしました！！ぜひ、遊んでみてください～。

![paramaker_ogp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/686970/375dab78-d2f0-6d95-3cfa-a13a053a1261.png)

パラメーカーに関するお問い合わせは、パラメーカーの[お問い合わせ](https://paramaker.net/contact/)か、[僕のツイッター](https://twitter.com/kanoi_y)までお願いします～。