SNSなどで動的URLを共有した時に、OGPを表示する開発でハマったポイント3選【nuxt.js(SPA) × Firebase】【個人開発】

こんにちは、Kigiと申します。
個人開発者同士でサービス・アプリを使いあってレビューを送り合うサービス【sougo-review】を運営しています。

https://sougo-review.com/

公開時に投稿した記事はこちらです。合わせてご覧ください。

https://qiita.com/_kigi__/items/7b652a081828a367fe16

# 登録したサービス・アプリページを共有するとOGPが表示されるように機能改善しました :clap:
公開してから細かいレイアウト修正などを行ってきましたが、この度、「サービス・アプリページのリンクを共有した時にOGPが表示される」ように機能改善しました！
下記リンクが実際にサービスページを共有した様子です。
どちらも私が開発・投稿したサービスになります

https://sougo-review.com/detail/kNiJxTVNRzfk88up1DLw

https://sougo-review.com/detail/90X1Xhrp8WKgEFCTjVoy

これによって投稿したサービス・アプリをTwitterなどのSNSで共有した際に自分で設定した画像やサービス名が表示され、より高い宣伝効果が得られると考えています！

サービス・アプリページのリンクは[ドメイン/detail/〇〇]と〇〇の部分が動的です。
この動的リンクを共有した時にOGPが表示されるための開発をして、実際に僕がハマったポイントを共有します！！

# 技術スタック
* Frontend
    * nuxt.js (2.15.7)
* Backend
    * Firebase(Functions)
* Hosting
    * Firebase(Hosting)

# OGPを実現するために参考にしたページ
以下の記事を参考にすると、nuxt.js(SPA)でもOGPを実現することができると思います。

https://www.memory-lovers.blog/entry/2019/08/07/150000

https://qiita.com/kanoi_y/items/c71a967673ce7944409d

# ハマったポイント3選
上記、記事を参考にしてもOGPが実現できなかった場合、以下の点を確認すると実現できるかもしれないです。
### OGPを返すfunctionsをindex.js以外に書いている場合には、firebase.jsonの書き方に注意する
プロジェクトのフォルダ構成はこのようになっていました。
```
root/
┣ functions/
│ └ opg/
│ │ └ ogp.js (1)
│ └ index.js (2)
│
└ pages/
│ └ ...
│
└ firebase.json
...
```

それぞれOGPのheadを返すコードはこのようになっています。
```root/functions/ogp/ogp.js
const functions = require('firebase-functions')
const admin = require('firebase-admin')

const createHtml = (TITLE, DESCRIPTION, OGP_URL, PAGE_URL, REDIRECT_URL) => {
  return `<!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width,initial-scale=1.0">
      <title>${TITLE} - sougo-review</title>
      <meta property="og:title" content="${TITLE} - sougo-review">
      <meta property="og:image" content="${OGP_URL}">
      <meta property="og:description" content="${DESCRIPTION}">
      <meta property="og:url" content="${PAGE_URL}">
      <meta property="og:type" content="article">
      <meta name="twitter:site" content="${BASE_URL}">
      <meta name="twitter:card" content="summary_large_image">
      <meta name="twitter:title" content="${TITLE} - sougo-review">
      <meta name="twitter:image" content="${OGP_URL}">
      <meta name="twitter:description" content="${DESCRIPTION}">
    </head>
    <body>
    <script type="text/javascript">window.location="${REDIRECT_URL}";</script>
    </body>
  </html>
  `
}


const BASE_URL = 'https://sougo-review.com'
const IMAGE_URL = '{省略}'

exports.serviceOgpFront = functions.https.onRequest(async (req, res) => {
  const db = admin.firestore()
  try {
    const [, , serviceId] = req.path.split('/')
    if (!serviceId) throw new Error(`serviceId is empty`)
    const docRef = db.collection('service').doc(serviceId)
    const snap = await docRef.get()
    if (!snap.exists) throw new Error(`Not Found: serviceId=${serviceId}`)
    
    // firestoreから表示するサービスのデータを取得する
    const service = snap.data()
    const title = `${service.name}`
    const desc = `${service.concept}`
    const ogpURL = service.image ? service.image : IMAGE_URL;
    const pageURL = `${BASE_URL}/detail/${serviceId}`
    const redirectURL = `/_detail/${serviceId}`

    const html = createHtml(title, desc, ogpURL, pageURL, redirectURL)

    res.set('Cache-Control', 'public, max-age=600, s-maxage=600')
    res.status(200).end(html)
  } catch (err) {
    console.warn(err)
    res.redirect('/')
  }
})
```

```root/functions/index.js
const functions = require('firebase-functions')
const admin = require('firebase-admin')

const ogp = require('./ogp/ogp')

admin.initializeApp()
module.exports = {
  ogp
}
```

functions/index.jsに関数を直接書くのではなく、別ファイルに処理の本体を書いて、index.jsでインポートする構成を採用しています。採用理由としては今後の機能拡張の際に、ogp表示以外の処理をfunctionsに記載する可能性があることを考慮しました。できる限りindex.jsにはコードを書かないことを意識しています。

この後、root/firebase.jsonを編集して、OGPを表示したいページにアクセスがきた時にfunctionsの関数を呼び出すようにhostingのrewirtes項目を設定します。この時に注意しなければならないのが、上記のようにindex.js以外のファイルにfunctionsの処理を記載している場合には、**{モジュール名}-{関数名}** と書く必要があります。
下記コードの「ogp-serviceOgpFront」が対象の部分です。

```root/firebase.json
{
  "functions": {
    "functions": {
      "source": "functions"
    }
  },
  "hosting": {
    "public": "dist",
    "trailingSlash": false,
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "/detail/*",
        "function": "ogp-serviceOgpFront"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
```



### nuxt.config.jsのgenerate項目を見直す
次はnuxt.config.jsです。まずrouter側でリダイレクトするように設定します。
```nuxt.config.js
export default {
  ssr:false,
  target: "static",
  router: {
    extendRoutes(routes, resolve) {
      routes.push({
        path: "/_detail/:serviceId",
        redirect: "/detail/:serviceId",
        chunkNames: {}
      });
    }
  },
  // 以下省略
}
```

ハマったポイントの2点目は同ファイルの[generate]項目を見直します。
SPAモードで動的URLの場合、何も設定しない場合対象画面で画面をリロードすると404やエラー画面に遷移するケースがあります。これの対策のためにgenerateのroutesに404を設定している方がいると思います。しかし、この場合だとOGPを表示させるためのfuncitonsを設定するとエラーが発生するため、routes項目を削除して、fallbackをtrueとして追加します。
```diff_javascript
export default {
  ssr:false,
  target: "static",
  router: {
    extendRoutes(routes, resolve) {
      routes.push({
        path: "/_detail/:serviceId",
        redirect: "/detail/:serviceId",
        chunkNames: {}
      });
    }
  },
  generate: {
    dir: "dist",
-   routes: ['404'],
+   fallback: true
  },
  // 以下省略
}
```
OGPを設定した後、画面をリロードした時の挙動がおかしい時にはnuxt.config.jsの設定を見直すと改善される可能性があります。

### 動作の確認の仕方は[firebase serve]コマンド実行→ブラウザで[ページのソースを表示]
最後はコードの話ではなく、実現されたかの動作確認の仕方です。
まず、下記コマンドを実行してビルド->静的ファイルを生成します。
```shell
npm run generate
```
次に動作を確認するために下記コマンドを実行します。
```shell
firebase serve 
```
成功すると、http://localhost:5000 などで対象のページをテスト環境で確認することができます。この時、functionsで設定したogpの関数も動作するようになっています。localhost:5000にアクセスした後、OGPを表示させる対象のページに移動します。
その後、head項目に値が設定されているかを確認するには、ブラウザの検証ツールではなく、[ページのソースを表示]でheadを確認します。
ブラウザの検証ツールではリダイレクトした後のhead項目が表示されているため、動作していないように見えます。しかし、ページのソースを表示で動作を確認すると想定のhead項目が帰って来ていることを確認できると思います。

# まとめ
この記事ではnuxt.js(SPA) × firebaseのwebサービスで動的URLを共有した時にOGPを表示させるための開発を行なった際にハマったポイントを紹介しました。
個人開発では、SNSでのシェアをマーケティングの施策にすることが多いと思います。その際にリッチなOGPを表示することでクリック率が上がることが期待できます。
少しでも困っている方の参考にあれば幸いです

# sougo-reviewについて
サービスページ

https://sougo-review.com/

Updateページ(更新履歴などを投稿しています。)

https://sougo-review-article.vercel.app/


# 参考記事

https://www.memory-lovers.blog/entry/2019/08/07/150000

https://qiita.com/kanoi_y/items/c71a967673ce7944409d

# 
本記事はQiitaイベント[エンジニア夏休み企画！]の個人開発の成果として作成しました。

https://qiita.com/official-events/6d31965c499a69377c0b