Nuxt + Firebaseで読書感想文を書けるWebアプリを開発しました

# こんなサービスを作りました

『みんなの感想文』というWebアプリをリリースしました！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">『みんなの感想文』<br><br>インターネット上で読書感想文を書けるサービスをリリースしました！<br><br>紙とペンがなくても感想文を書ける、そしてアーカイブとして残す世界を実現したくて作りました。400文字か200文字の原稿用紙で書くことができるので、みんなも書いてみてください📖<a href="https://t.co/DkCThMPpPl">https://t.co/DkCThMPpPl</a></p>&mdash; フジワラユウタ@みんなの感想文 (@Fujiyama_Yuta) <a href="https://twitter.com/Fujiyama_Yuta/status/1182072317217628160?ref_src=twsrc%5Etfw">October 9, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# この記事について

今月から個人でサービスを開発・運営をしている人たちの組織「[運営者ギルド](https://qiita.com/organizations/admin-guild)」のOrganizationの一員として所属することになりました。

所属してから初めてのサービスローンチなので、忘れないうちに作った開発技術などを備忘録として残しておきます。合わせて個人開発の運用にかかっている費用感なども公開します。個人でサービスを開発をしてみようと考えている人にとって役立つような情報を書ければいいなと思っています。

## どんなサービスか？

一言で説明すると縦書きの感想文がWebブラウザで作成できるサービスです。400文字 or 200文字の原稿用紙のどちらかで、感想文を書くことができ、入力した文字がリアルタイムで反映されていきます。読書感想文の場合、読んだ本の情報をAmazonAPIから取得して、出版社・著者・本の金額・Amazonへの商品リンクなどを追加することができます。

<img width="1276" alt="スクリーンショット 2019-10-14 0.12.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/11932d06-eaa4-9ddf-0c76-561ae246aeb3.png">

## 使っている技術

* Nuxt.js
    * SPAモード
* Firebase
     * Hosting
     * Firestore
     * Authentication
     * Storage
     * FirebaseFunctions
* Product Advertising API(Amazonの商品取得)
* Github


このサービスはSNS等でシェアしたときにOGimageで読書感想文の一部を表示できるような仕組みを取り入れたかったので、開発当初はNuxt + Herokuで環境を構築しSSRをしてmeta情報を書き換えるという実装をしていました。開発している途中にOGimageを表示するために、わざわざSSRをする必要はあるのか？Herokuのリージョンは日本にないのでキャッシュ等を上手くつかわないとレイテンシが半端ない？という懸念点から、ホスティングはFirebaseHostingを使って、全てFirebaseのプラットフォームを使うように設計を変更しました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">シェアはこっちか。書いてみた。『消えた活字の冒険』<a href="https://t.co/YxH4AcXWHr">https://t.co/YxH4AcXWHr</a> <a href="https://twitter.com/hashtag/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AE%E6%84%9F%E6%83%B3%E6%96%87?src=hash&amp;ref_src=twsrc%5Etfw">#みんなの感想文</a></p>&mdash; shosira / Shoji Ohashi (@shosira) <a href="https://twitter.com/shosira/status/1182250834458898433?ref_src=twsrc%5Etfw">October 10, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

※ Regionはasia-northeast1（東京）を使用
※ FirebaseFunctionsのみus-central1（米国）を使用（カスタムドメインの設定のため）

## システム構成

システム構成は以下のような感じになっています。
![システム構成.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/9f762798-7d04-dc02-ce44-130f5ea394a0.jpeg)


## 運用コスト

* ドメイン - kansobun.jp - **2,340円/年** (お名前.com)
* Firebase - Blazeプラン(従量課金のプラン) - **5円/月**

<img width="994" alt="スクリーンショット 2019-10-14 8.38.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/221c191c-ccad-5686-9c1d-047f2f9ccc94.png">

数ヶ月運用してみないとわかりませんが、今のとこ流入もそんなに多くないのでコストもほとんどかかっていない感じです。


## 開発で苦戦したところ

### 最初に**us-central1**(米国アイオワ州)リージョンを選択してしまった

* レイテンシがすごい（光遅い問題）
* リージョンは一度設定してしまうと後から変更できない
* **us-central1**から**asia-northeast1（東京）**に移行するのは結構大変（ドメインの付け替え、Firestoreのインデックスの作り直し等）
* **教訓：日本からのアクセスが多いサービスの場合asia-northeast1からのレンポンスが絶対的に早いためasia-northeast1を選択したほうが良い(と思います)！**

### FunctionsからProduct Advertising API(Amazon)が叩けなかった
* **教訓：Blazeプラン（従量課金）でないと外部のドメインとの接続が許可されていない**

### Functionsのカスタムドメイン設定のリージョンは**us-central1**のみ
* エンドポイントのURLをカスタムドメインにしたいですよね。デフォルトだと`https://us-central1-example.cloudfunctions.net/kansobun` ですが`https://example.com/api/kansobun` でAPIを呼びたい。そしてasia-northeast1リージョンを使いたい。しかし、それはできないのです...。
* **教訓：Functionsはasia-northeast1で使用することも可能。ただしカスタムドメインで設定する場合はus-central1のみ**

<img width="963" alt="スクリーンショット 2019-10-14 10.35.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/8e71731e-59a2-0d93-b562-6f1cd2a71dbe.png">

### Product Advertising APIの審査がかなり厳しい
本の情報を取得するためにAmazonが公開しているProduct Advertising APIを使う必要がありました。このAPIの利用には審査があり、その審査の基準がブラックボックス化されているため、なぜ審査に落ちたのかがわかりませんでした（私の場合14回落ちて、15回目で通った）。以下を修正するとことで審査に合格することができたので、振り返り返ってみました（自分調べなので、他の要因もあると思うので、ご参考までに）。

* 独自ドメインのサイトでないと審査が通らない
* サイトのコンテンツが5〜10つ必須
* metaタグの情報が必須
* **教訓：開発中のサイトでの審査は通らないので、ほぼ完成した段階で申請しないと無駄な時間を費やしてしまう**

## 工夫した部分

### SPAでOGimageを動的に変える

シングルページアプリケーションでOGimageを表示を動的に変更させるために少しだけ工夫をしました。
※ 以下の記事を参考にさせていただきました。
[Nuxt.js + FirebaseでOGPの仕組みを完全に理解した　〜俳句をSVGで描画するサービスをリリースした話〜](https://qiita.com/mitsudaman/items/1956b94dc8faf8fb8c59) - @mitsudaman

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">チャレンジしたエンジニアだけがバグを生むので、ダサイおさむさんはきっとチャレンジしているエンジニアだと思う。Webpack難しいのは激しく同意。<br><br>『エンジニア失格』の読書感想文。 <a href="https://t.co/eNlT4ylqfY">https://t.co/eNlT4ylqfY</a> <a href="https://twitter.com/hashtag/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AE%E6%84%9F%E6%83%B3%E6%96%87?src=hash&amp;ref_src=twsrc%5Etfw">#みんなの感想文</a></p>&mdash; フジワラユウタ@みんなの感想文 (@Fujiyama_Yuta) <a href="https://twitter.com/Fujiyama_Yuta/status/1182177216534200321?ref_src=twsrc%5Etfw">October 10, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このOGimageを表示するために以下のFunctionsを実行しています。

```JavaScript:share.js
const functions = require('firebase-functions');

module.exports = functions.https
  .onRequest((req, res) => {
    let SITEURL = 'og:url';
    let TITLE = 'og:title';
    let DESCRIPTION = 'og:description';
    let IMAGE = `https://kansobun.jp/main_ogp.jpg`;

    let itemParams = '';
    let docUserItems = fireStore
      .collection('kansobun')
      .doc('items')
      .collection(itemId)
      .get()
      .then(doc => {
        if (doc.exists) {
          itemParams = doc.data();

          TITLE = 'みんなの感想文 | ' + itemParams.title;
          SITEURL = 'https://kansobun.jp/post/?id=' + itemId;
          DESCRIPTION = itemParams.text;
          IMAGE = itemParams.image_url;
          res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
          res.status(200).send(`<!doctype html>
          <script>window.location.href="${SITEURL}"</script> // ** ここがポイント！
          <head>
            <title>${TITLE}</title>
            <meta property="og:title" content="${TITLE}">
            <meta property="og:image" content="${IMAGE}">
            <meta property="og:description" content="${DESCRIPTION}">
            <meta property="og:url" content="${SITEURL}">
            <meta property="og:type" content="website">
            <meta property="og:site_name" content="${TITLE}">
            <meta property="fb:app_id" content="123456789012345" />
            <meta name="twitter:site" content="@kansobun20">
            <meta name="twitter:card" content="summary_large_image">
            <meta name="twitter:title" content="${TITLE}">
            <meta name="twitter:image" content="${IMAGE}">
            <meta name="twitter:description" content="${DESCRIPTION}">
            <meta name="twitter:url" content="${SITEURL}" />
            <link rel="canonical" href="${SITEURL}">
          </head>
          <body>
          </body>
        </html>`);
        } else {
          itemParams = 'NOTHING';
          res.status(404).send(itemParams + '：' + itemId);
        }
      })
      .catch(error => {
        res.status(400).send(error + '：' + itemId);
        console.error('Error getting document:', error);
      });
  });

```

詳細ページのURL `/post/hogehugapiyo` と、OGimageを表示させるシェア用のURL`/share/hogehugapiyo`を用意します。`/share/hogehugapiyo`はFunctionsでmata情報だけをレンダリングしたhtmlを返すAPIになっており、Twitterの場合はJavaScriptが実行されないためmeta情報だけがレンダリングされたページがクローリングされ動的OGimageが表示されます。人間の場合は`/post/hogehugapiyo`にリダイレクトされ、詳細ページの情報が表示されるような仕組みなっています。

![システム構成シェア.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/42d8ed9d-ccc7-ba2b-3d63-4ea348b4301b.jpeg)


## 所感

### もうSPAでいいんじゃないか。

今回はSSRモードではなく、SPAモードを使用しました。色々なところで話題に上がる「**SPAってSEO的にどうなの？**」という問題がありますが、Googleの最新のクローラーのレンダリングエンジンは最新版Chromium相当の機能を持ち合わせており、JavaScriptも実行できると言われています。実際にGoogleBotがどのようにページを表示していつかをサーチコンソールで確認することができますが、SPAのアプリケーションでも問題なくコンテンツが表示されていることを確認しました。ただ世界にはGoogle以外にも多くのクローラーが巡回しているので「全部SPAだけでいいんじゃないか？」というわけいはいかなそうです。そういったクローラーに対してはサーバーサイドでレンダリングしたhtmlを返す必要があるのでSSRをする必要があります。（が、全てを網羅することは難しいので個人的にはSPAでいいんじゃないかという結論に達しています...）

<img width="995" alt="スクリーンショット 2019-10-14 21.03.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/b6c8ba69-e792-a407-a8b7-d19054cb9a11.png">

[Googlebotのレンダリングが最新版Chrome相当になった！ これは嬉しい！【SEO記事13本まとめ】](https://webtan.impress.co.jp/e/2019/05/24/32731)
[アップデートでレンダリングエンジンが改良　ES6などがサポート対象に](https://ferret-plus.com/11957)

### Firebaseはやっぱり最高！

やっぱりFirebaseはすごいですね。本当に便利です。FirebaseHostingを使えばSSLやDNSを一瞬で設定することができます。初めて使ったとき「こんな簡単にアプリケーションが動くのか！」と感動しました。

アプリケーションはVPSに乗せて、データストアだけFirebaseを使うなど部分的に使うこともできたりするので、色々な使い方を試してみてください。

## まとめ

みんなの感想文は読書感想文だけではなく、日常のポエムや、映画や音楽の感想文など色々なジャンルの投稿をすることができます。ぜひ、ささいなことでもいいので400文字or200文字でみんなの感想文を投稿してみてください！

![main_ogp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147291/0879b1ad-3488-b874-318e-4ed7c262e2de.jpeg)


[みんなの感想文 - kansobun.jp](https://kansobun.jp)