Nuxt.js + Firebase Hosting + CloudFunctionsでmspeakerdeckをSSR対応させる

自分はSpeaker Deckのモバイルクライアントであるmspeakerdeckというwebアプリを作っています。

mspeakerdeckを知らない人のために書いておくと、

これはSpeaker DeckのスライドページのURLドメインmを付与するだけでモバイル版の見やすいレイアウトになるというものです。

PCからのアクセス時はSpeaker Deckのオリジナルサイトが表示され、スマホからのアクセス時はmspeakerdeckのサイトが表示されるという仕組みになります。

当初はNuxt + Cloud Functions + Nuxt.jsのPWAとしてSSR無しの完全SPAで作成していたのですがしばらく使ってみるといくつか不便な点が見つかりました。

その一つがTwitterでシェアした時やはてぶしたときにうまくスライドの内容を取得してくれないというものです。

自分ははてぶとTwitterシェアを特によく利用するのでこれはとても不便でしたので、今回は面倒とわかってはいながらも泣く泣くSSR対応をさせることにしました。

やり方

基本的な方針は、

  • サーバーサイド用のコードとフロント用のコードを用意する
  • 一つのリポジトリ内にsrc(フロント用)とfunctions(サーバー用)のディレクトリを準備してアプリを分ける
  • デプロイはfirebase.jsonにfirebase hostingとcloud functions用の設定を書いてfirebase deployで一発でできるようにする

という流れです。

具体的な実装はこの記事の通りにやるだけで基本的には大丈夫なので割愛します。

リポジトリもここで確認できるので参考にしてみてください。

github.com

ハマりどころなど

基本的にはこの記事の通りで問題ないですが、一部ハマった部分などがあるので書いておきます。

Nodeのバージョン

信じられないかもしれないですがCloudFunctionsではNodeのバージョンが6系しか使えないのでNuxt.jsも1系のrcバージョンを使わざるをえません。

自分はもともとNuxt v1.4系を使っていたので利用していたNodeのバージョンが8系でした。

このことを忘れてローカルでnpm installなどをしていたので当初は謎のエラーが出てサーバーが立ち上がらないなどという問題に当たりました。

既存のアプリをCloudFunctions向けにSSR対応させる時はNodeのバージョンに気をつけてください。

asyncDataで取得したデータをコンポーネントで利用する

NuxtはSSRで実行してほしい処理をasyncDataというオプションで定義できます。

具体的には下記のような感じで、asyncDataにデータでスライドデータを取得する処理を書きます。

そうするとcloud functions側でこの処理が走るので結果的にSSRできるというわけです。

<template>
  <div>
    <Header></Header>
    <Slides :prms="prms" :item="item"></Slides>
  </div>
</template>

<script>
import Header from "~/components/Header";
import Slides from "~/components/Slides";
import axios from 'axios';
export default {
  components: {Header, Slides},
  asyncData({ params }, callback) {
    let vm = this;
    const SPEAKERDECK_URL = `https://speakerdeck.com/${params.user}/${params.slideName}`;
    const CORS_SERVER_URL = "https://example.com/getSpeakerdeckThumb";
    return axios({
      method: 'post',
      url: CORS_SERVER_URL,
      data: {
        url: SPEAKERDECK_URL,
      }
    }).then((resp) =>{
      callback(null, {
        prms: params,
        item: {
          title: resp.data.title,
          slideId: resp.data.id,
        },
      });
    }).catch((e) => {
      callback({ statusCode: 404, message: `${e}` });
    });
  },
}
</script>

<style lang="scss" scoped>
</style>

便利なasyncDataですが、これはNuxtのpages/配下でしか利用できません。

asyncDataで取得した結果をコンポーネントで使いたい場合はpages/からコンポーネントにpropsで渡すようにします。

上記のコードではcallbackでitemとprmsというデータを返すようにして、templateのにpropsとして渡している部分になります。

初歩的なとこですが忘れていてコンポーネント側で処理を書いてしまうということのないように気をつけてください。

メタタグ

自分の場合はogタグとtwitterタグをスライドのページでのみ設定するようにしました。

メタタグの設定はheadオプションを使って行います。

具体的には下記のようなコードです。

head () {
  const ogImg = `https://speakerd.s3.amazonaws.com/presentations/${this.item.slideId}/slide_0.jpg`;
  return {
    title: `${this.item.title} // mspeakerdeck`,
    meta: [
      { hid: "og:title", name: "og:title", property: "og:title", content: this.item.title },
      { hid: "og:url", name: "og:url", property: "og:url", content: `https://mspeakerdeck.com/${this.prms.user}/${this.prms.slideName}` },
      { hid: "og:image", name: "og:image", property: "og:image", content: ogImg },
      { hid: "twitter:card", name: "twitter:card", property: "twitter:card", content: "summary_large_image" },
      { hid: "twitter:site", name: "twitter:site", property: "twitter:site", content: "@razokulover" },
      { hid: "twitter:title", name: "twitter:title", property: "twitter:title", content: this.item.title },
      { hid: "twitter:description", name: "twitter:description", property: "twitter:description", content: "Unofficial mobile viewer for Speaker Deck" },
      { hid: "twitter:image:src", name: "twitter:image:src", property: "twitter:image:src", content: ogImg },
    ]
  }
},

SSRでやる場合はwindowやdocumentが使えないので注意してください。

フロント側で利用しているパッケージはfunctionsでもインストールする必要がある

例えばフロント側でaxiosを使っていますが、この場合はfunctions/package.jsondependencyにもaxiosを設定しないといけないです。

srcとfunctionsの二箇所でpackage.jsonを管理しないといけないのでやや煩雑ですね。

※こういうアドバイスをいただいた

表示速度が落ちた

これはSSRあるあるですが、特にmspeakerdeckのような外部のAPIを呼ばないといけないようなアプリの場合は特に初回の表示速度が落ちてしまいます。

一応Firebase Hosting側でcloudfunctionsのレスポンスをキャッシュしたりできるのですがそれでもやはり完全にSPAだったときよりはもっさりしてる感は否めません。

うまい解決方法があれば良いですが、残念ながら未だ課題のままです。

はてなブックマークのアプリ

Twitter,FacebookはCardの情報を取得するのにUAがPCでかつFacebookTwitterであることを示したbotを使っているので、

実は無理してメタタグを埋め込む必要はないです。

というのも、cloud functions側ではPCからのアクセス時にSpeaker Deckにリダイレクトしており、botはPCからのアクセスという扱いなのでなんとSpeaker DeckのOGPを使ってくれます。

しかしながらはてぶのアプリの場合はページの情報をモバイルアクセスで、しかもはてなとわからないUAでメタタグを取得するのでSSRが必須になります。

なのでいわばSSRははてぶのアプリのためにやっているようなものです。

なんとかならないですかねこれ...。

まとめ

  • mspeakerdeckの改修をしました
  • Firebase Hosting + CloudFunctions + NuxtでSSRするようにしました
  • 表示速度が落ちたりはしたがOGPを無事に認識させられるようになった

今後も地道に開発運用していくのでmspeakerdeckを使ってみてください。

何かあればブコメ@razokuloverまでお願いします。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

  • 作者:mio
  • シーアンドアール研究所
Amazon