NuxtとFirebaseでTwitterOGP使ったPWAアプリを最速で開発

[OneSmallStep](https://1smallstep.jp)CTOの西([@_takeshi_24](https://twitter.com/_takeshi_24))です。

## TwitterOGP使ったPWAアプリリリースしました
[Peing](https://peing.net/)や[bosyu](https://bosyu.me/)などTwitterのOGPを利用したアプリが最近色々と人気です。

私もTwitterのOGPを使って、目標を宣言し、シェアし、目標に向かって熱量高く頑張っている人同士をマッチングできるサービス「[達成.me](https://tassei.me)」をリリースしました。(**現在β版で、マッチング機能は近々リリース予定です。**)
<img width="1440" alt="TwitterOGPアプリ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/fcf9db66-f74b-dd6e-d7fc-84a5b659fa21.png">

このようなアプリにどれだけニーズがあるかわからないので、最小限の機能で最速で**MVP**(Minimum Viable Product)をリリースしました。
Nuxt.jsとFirebase(Firebase Authentication / Firestore / CloudFunctions)を使って、3日で初期バージョンを開発しましたので、そのノウハウを公開します！

Nuxt.jsとFirebaseについての詳細は私の[去年のアドベントカレンダー](https://qiita.com/advent-calendar/2019/diffeasy24)の記事を参考にしてください。

## アプリの機能
アプリに必要な機能は主に以下の通りです。

- PWA対応
- ログイン
- 入力された文字からOGP用画像の動的生成
- PWAでOGPの動的設定
- Twitter / Facebookシェアボタン
- 利用規約やプライバシーポリシー

これらの機能について１つずつ説明していきます。

## Nuxt.jsでPWA対応
Nuxt.jsの導入方法は[公式サイト](https://ja.nuxtjs.org/)に記載ありますが、「create-nuxt-app」スターターテンプレートを使ってインストールしまう。
パッケージ管理に[Yarn](https://legacy.yarnpkg.com/lang/ja/)を使いますので、インストールがまだの方は、そちらの手順もWeb上にありますので、まずはそちらを参考にしてインストールしてください。

スターターテンプレートを利用したアプリの生成方法は「[Nuxt.jsとFirebaseとCloudFunctionsでWebアプリ開発 - 準備編](https://qiita.com/_takeshi_24/items/c26e4a664e27b652d9a6)」こちらの記事を参考にしてください。

いくつかポイントだけ説明します。

UIフレームワークは今回「[Vuetify.js](https://vuetifyjs.com/ja/)」を利用しました。
こちらは使わなくても良いですし、他のUIフレームワークでも構いません。

Axios、PWA、.envなど利用するモジュールを聞かれますが、今回は「PWAアプリ」のみ利用します。

SSR(サーバーサイドレンダリング)かSPA(Single Page App)はSPAを選択します。

PWA対応のため、nuxt.config.jsに以下の内容を追加します。

```javascript:nuxt.config.js
export default {
  // ・・・
  manifest: {
    name: "達成.me",
    short_name: "達成.me",
    title: "達成.me",
    lang: "ja"
  },
```

PWA用アイコンのための画像を```/static/icon.png```に配置します。

## Firebaseの準備
今回サーバーサイドは[Firebase](https://firebase.google.com/?hl=ja)のFirebase Authentication、Firestore、CloudFunctionsを利用しますので、そのための準備をします。

この手順も「[Nuxt.jsとFirebaseとCloudFunctionsでWebアプリ開発 - 準備編](https://qiita.com/_takeshi_24/items/c26e4a664e27b652d9a6)」を参考にしてください。

ポイントだけ説明します。

利用するサービスを選択する箇所は、今回はRealtimeDatabaseは利用しないので、それ以外のFirestore、Functions、Hosting、Storage、Emulatorsを選択します。

エミュレータも同様に、Firestore、Functions、Hostingを選択します。

## Firebase Authenticationでログイン
ログインはEmailログイン、Twitterログイン、Facebookログインを利用します。

Twitterログイン、Facebookログインを利用するためには、それぞれTwitter、Facebookの開発者ページでアプリを登録する必要があります。

### Twitter開発者登録
Twitterアプリを登録するためにはまずは開発者登録する必要があります。これがなかなか面倒です・・
[Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)

英語で申請が必要になります。
申請の理由なども必要なのですが、「WebアプリでTwitterログインを利用したいです。ユーザーのツイートやフォロワー情報を取得したり、アプリから自動でTwitterに投稿したりすることはありません。」ということをGoogle翻訳を駆使して、申請しました。

申請するとメールが届き、そのメールに返信する形で再度申請理由を送付すると、アプリ開発者認定され、[Twitter開発者ページ](https://developer.twitter.com/)からアプリを登録できるようになります。

### Twitterログイン
次にFirebaseのコンソールで、「Authentication」→「ログイン方法」を選択し、Twitterログインを開き、「有効にする」をオンにします。
<img width="973" alt="Twitterログイン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/dd290e7a-2146-738c-f3bc-69d51f7c0d36.png">

Twitterアプリ設定で利用するので、コールバックURLをコピーします。

[Twitter開発者ページ](https://developer.twitter.com/)画面右上のメニューから「Apps」を開きます。
<img width="430" alt="Twitter開発者ページ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/7bed091b-ab0b-9109-04fe-245432a09536.png">

「Create an app」ボタンから、新規アプリケーションを登録します。
必要事項を入力し、「Create」をクリックすると、アプリが登録されます。

「Keys and tokens」に「API key」と「API secret key」が表示されますので、これをコピーして、先ほどのFirebaseのTwitterログイン設定画面に入力し、「保存」します。

### Facebook開発者登録
Facebookアプリも開発者登録が必要です。
[このあたりのページ](https://requlog.com/self-branding/wordpress/facebook-app-id-app-secret/)を参考に、開発者登録してください。

### Facebookログイン
Firebaseのコンソールで、「Authentication」→「ログイン方法」を選択し、Facebookログインを開き、「有効にする」をオンにします。
<img width="973" alt="Firebaseログイン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/4d99b9ef-2464-8478-d00a-465f286733b1.png">
Facebookアプリ設定で利用するので、コールバックURLをコピーします。

[Facebook開発者ページ](https://developers.facebook.com/?locale=ja_JP)のメニューから「アプリを作成」をクリックします。
<img width="722" alt="Facebookアプリ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/02d52346-0805-b8ff-eea1-93c670bd6f39.png">

アプリ名とメールアドレスを入力してアプリを作成します。

「製品を追加」から「Facebookログイン」の「設定」をクリックします。
<img width="962" alt="スクリーンショット 2020-01-26 16.49.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/f4656dde-bdb7-a7d2-472f-8b47a480747b.png">
「ウェブ」を選択します。

<img width="959" alt="スクリーンショット 2020-01-26 16.50.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/3e054cbf-2643-114c-1d0a-001f17585264.png">
ウェブサイトの情報を入力します。
Firebase Authenticationを利用するので、2〜5の手順は不要です。

左側メニューの「Facebookログイン」→「設定」を開きます。
「有効なOAuthリダイレクトURI」に先ほどコピーしたリダイレクトURLを貼り付けます。

左側メニューの「設定」→「ベーシック」を開き、必要な情報を入力します。（利用規約やプライバシーポリシーについては以降の「利用規約やプライバシーポリシーどうする？」を参考にしてください。）
<img width="961" alt="スクリーンショット 2020-01-26 17.16.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/ddb89ab6-b5e0-78cb-bc76-0f044493081c.png">

設定が完了したら、画面上のステータスをオンにします。

画面の「アプリID」と「app secret」の値をコピーし、先ほどのFirebaseのFirebaseログイン設定画面に入力し、「保存」します。

### メールパスワードログイン
メールパスワードログインを有効にします。
<img width="971" alt="メールパスワードログイン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/699de428-7a9c-9ab2-f7e7-c5ae65db96c8.png">

### FirebaseUIを利用したログイン画面
ログイン画面もFirebaseUIを利用すると、簡単に実装できます。

ログイン画面の開発については「[Nuxt.jsとFirebaseAuthentication/FirebaseUIでログイン認証](https://qiita.com/_takeshi_24/items/4ea05d5564a5d5ca9825)」を参考にしてください。

メール、Twitter、Facebookの３種類のログインを利用するので、```/plugins/firebase.js```の設定を以下のように変更してください。

```javascript:/plugins/firebase.js
export const authProviders = {
  Email: firebase.auth.EmailAuthProvider.PROVIDER_ID,
  Twitter: firebase.auth.TwitterAuthProvider.PROVIDER_ID,
  Facebook: firebase.auth.FacebookAuthProvider.PROVIDER_ID
};
```

これでログイン機能の開発は完了です。

## OGP用画像を動的に生成する
画面で入力された文字を事前に用意した画像内に埋め込み、動的にOGP用の画像を生成します。
この処理はCloudFunctionsを利用します。
Nuxt.jsとCloudFunctionsの連携について、基本的な手順は「[Nuxt.jsとCloudFunctionsでサーバーレスアプリ開発入門編](https://qiita.com/_takeshi_24/items/5adc9b91bc7173dd5abb)」を参考にしてください。

OGPの動的作成については「[Cloud Functions + ImageMagickでOPG画像の動的生成してCloud Storageにアップロードする](https://www.memory-lovers.blog/entry/2019/06/26/194500)」こちらの記事が参考になりました。

背景画像と、フォントは事前に用意します。
背景画像は、CloudFunctionsソースの```functions/img/ogp.png```に配置し、フォントは```functions/font/```配下に配置します。

生成されたOGP画像はFirebaseのStorageに保存されます。

OGP画像のURLをFirestoreのデータベースに保存します。

## PWAでOGPを動的に設定する
PWAではページごとに動的にOGPを設定することはできません。
サイト全体のOGP設定は```/nuxt.config.js```で設定できます。
以下のような感じで設定します。

```javascript:/nuxt.config.js
  head: {
    htmlAttrs: {
      prefix: "og: http://ogp.me/ns#"
    },
    titleTemplate: "%s - 達成.me",
    title: "目標と期限を宣言し、達成する | 達成.me",
    meta: [
      { charset: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      {
        hid: "og:title",
        property: "og:title",
        content: "目標と期限を宣言し、達成する | 達成.me"
      },
      { hid: "og:site_name", property: "og:site_name", content: "達成.me" },
      {
        hid: "og:description",
        property: "og:description",
        content:
          "目標と期限を宣言。みんなで応援して仲間を見付けて達成するためのサービス。達成.me。"
      },
      { hid: "og:url", property: "og:url", content: "https://tassei.me" },
      {
        hid: "og:image",
        property: "og:image",
        content: "https://tassei.me/icon.png"
      },
      {
        hid: "twitter:site",
        property: "twitter:site",
        content: "@tasseiMe"
      },
      {
        hid: "twitter:creator",
        property: "twitter:creator",
        content: "@tasseiMe"
      },
      {
        hid: "twitter:title",
        property: "twitter:title",
        content: "目標と期限を宣言し、達成する | 達成.me"
      },
      {
        hid: "twitter:image",
        property: "twitter:image",
        content: "https://tassei.me/icon.png"
      },
      {
        hid: "twitter:description",
        property: "twitter:description",
        content:
          "目標と期限を宣言。みんなで応援して仲間を見付けて達成するためのサービス。達成.me。"
      }
    ],
    link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }]
  },
```

問題は、Twitter、Facebookでシェアしたい、動的にOGPを設定したいページです。
こちらについても、[@kira_puka](https://twitter.com/kira_puka)さんの「[Nuxt(SPA)+FirebaseでSEO！OGP！: 特定のパスだけheadだけ返すやつ](https://www.memory-lovers.blog/entry/2019/08/07/150000)」の記事が参考になりました。

[@kira_puka](https://twitter.com/kira_puka)さんありがとうございます！

特定のURLへのアクセスのみ、CloudFunctionsに処理を流して、CloudFunctions側でheadだけのHTMLを生成しています。

これで、CloudFunctions側で、以下のようなHTMLを返します。

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>${TITLE}</title>
    <meta property="og:title" content="${TITLE}">
    <meta property="og:image" content="${OGP_URL}">
    <meta property="og:image:width" content="1200">
    <meta property="og:image:height" content="630">
    <meta property="fb:app_id" content="xxxxxxxxxxxxxxxxx">
    <meta property="og:description" content="${DESCRIPTION}">
    <meta property="og:url" content="${PAGE_URL}">
    <meta property="og:type" content="article">
    <meta property="og:site_name" content="達成.me">
    <meta name="twitter:site" content="@tasseiMe">
    <meta name="twitter:creator" content="@tasseiMe" />
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="${TITLE}">
    <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>
```

Twitterで以下のようにカードを表示するためには、以下の設定が必要です。

```html
<meta name="twitter:card" content="summary_large_image">
```

```html
<meta name="twitter:image" content="${OGP_URL}">
```

FacebookOGPでは、先ほど発行したアプリIDの設定が必要です。

```html
<meta property="fb:app_id" content="xxxxxxxxxxxxxxxxx">
```

<img width="603" alt="スクリーンショット 2020-01-26 18.07.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/eeab9152-a88f-a7ef-a272-60d14089fb7d.png">


## Twitter / Facebookシェアボタン
ページが用意できたら、シェアボタンを画面に設置します。
その前に、OGPが正しく効いているか、チェックしましょう。

###TwitterOGPのテスト
Twitterは[Twitter Card Validator](https://cards-dev.twitter.com/validator)を利用します。
URLを入力すると、以下のように結果が表示されます。
<img width="1440" alt="スクリーンショット 2020-01-26 18.10.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/4eb39110-016b-6505-ae9d-fabec67bda2a.png">

###FacebookOGPのテスト
Facebookは[Facebookシェアデバッガー](https://developers.facebook.com/tools/debug/)を利用します。
URLを入力すると、以下のように結果が表示されます。

<img width="993" alt="スクリーンショット 2020-01-26 18.23.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/ce8e076b-51ad-5532-19bc-24c0f5a65fac.png">

問題が表示されたら、OGPタグを修正してください。

###Twitterシェアボタン
Twitterシェアボタンを設置します。
リンクのhrefにシェア用のURLとパラメータを設定します。

```html
<v-btn
  :href="
    `http://twitter.com/share?url=${url}&text=${vision}&hashtags=達成me,${category}&via=tasseiMe&lang=ja`
  "
  class="twitter-share-button"
  target="_blank"
  depressed>
  <v-icon left>mdi-twitter</v-icon>
  <span class="d-none d-sm-flex">ツイッターで</span>目標をシェア
</v-btn>
```

パラメータについては以下の通りです。
url・・・シェアするURL
text・・・シェアするテキスト
hashtags・・・シェアするハッシュタグをカンマ区切りで
via・・・メンションするTwitterアカウント
lang・・・日本語なので、ja

###Facebookシェアボタン
Facebookシェアボタンを設置します。
リンクのhrefにシェア用のURLとパラメータを設定します。

```html
<v-btn
  :href="`http://www.facebook.com/share.php?u=${url}&t=${vision}`"
  class="facebook-share-button"
  target="_blank"
  depressed>
  <v-icon left>mdi-facebook</v-icon>
  <span class="d-none d-sm-flex">Facebookで</span>目標をシェア
</v-btn>
```

パラメータについては以下の通りです。
u・・・シェアするURL
t・・・シェアするテキスト

##利用規約やプライバシーポリシーどうする？
利用規約やプライバシーポリシーも必要になります。
個人開発の場合は、テンプレートがあるので、そちらを参考にしてください。

https://kiyaku.jp/

修正が必要な箇所は修正してください。
裁判管轄はご自身の居住地域の裁判所を指定してください。


##Firebaseの料金いくらかかる？
[Firebaseの料金プラン](https://firebase.google.com/pricing?hl=ja)は従量課金になりますが、料金表を見るとわかる通り、かなりアクセスが多いとか、変なループ処理がない限り格安です。
<img width="671" alt="スクリーンショット 2020-01-26 18.39.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003/6e69606e-f555-355e-d4c2-906ac0378215.png">

料金が気になる方は、GCPコンソールの「お支払い」→「予算とアラート」から予算とアラートを設定できます。

##今後の「達成.me」のリリース予定
「[達成.me](https://tassei.me)」がやりたいことは、目標の管理やTODO管理ではなく、同じ目標に向かって高い熱量で行動している人同士のマッチングです。

マッチング機能もまもなくリリース予定ですので、ぜひ「[達成.me](https://tassei.me)」で目標を宣言してシェアしてください！

##最後に
今後のアプリ改善のために、ぜひ簡単なアンケートにご協力をお願いします！
https://forms.gle/RYTYBfrsWHoXannW9

この記事が参考になった方は、記事の「いいね」や「ストック」をクリックしてもらえると嬉しいです。

Twitter([@_takeshi_24](https://twitter.com/_takeshi_24))のフォローもお願いします！