[Vue.js] Cloud FunctionsとStorageで動的なSitemapを作ろう

はじめまして。
マインドツリーを使ってゆるく考察するサービス<a href="https://qnqtree.com" target="_blank">Q&Q</a>を作っているあどにゃーです。今回はVue.jsのようなSPA(single page application)で動的ページを作ったけど、sitemapを毎回deployしないと更新できないのは面倒だって人向けのお話です。

#Sitemapとは
Sitemap(サイトマップ)は、サイト全体のページ構成を地図のように一覧で記載しているページのことです。検索エンジンに構成ページを伝えて検索にひっかかるようにしてもらうために用意します。Vue.jsのようなSPAでも、静的なページだけの場合は、Sitemapをわざわざ準備しなくてもクローラは各ページをインデックス（登録）してくれます。しかし、Vue.jsでuser/:userIdのような動的なページを作っている場合、Sitemapを作って明示しておかないとインデックスしてくれない場合が多いです。:sob:

#置き場の課題
Vue.jsの場合、Sitemapは/static/sitemap.xmlに置くのが一般的かなと思います。しかし、静的にsitemap.xmlを置いてしまうと、新しいページが生成された度にsitemapを更新してdeployすることになり手間がかかります。個人ブログなどでVue.jsを使っていて、新しいページの生成があまり頻繁に起きない場合はマニュアルでdeployすれば良いかもしれません。しかし、公開サービスで使っていてユーザが自由にページを生成できるような仕組みにしていると、マニュアルでSitemapを毎回deployするのはちょっと無理があります。:sob:
<a href="https://qnqtree.com" target="_blank">Q&Q</a>では、誰でもtree/treeIdを動的に作れる仕様になっているため、動的sitemapにしないと厳しい状況です。

###環境
Vue.js
Firebase
Firestore
Cloud Functions


#アプローチ
今回はCloud FunctionsとCloud Storageを使って動的Sitemapをクローラに読み込ませる方法を紹介します。アプローチとしては下記になります。
事前準備
1. 動的にsitemap.xmlを作成
2. Cloud Storageに作成したsitemap.xmlを保存
今回やる「検索クローラ対応」
1. firebaseのhostingで**/sitemap.xmlにアクセスしたらFunctions:updateSitemapに飛ばす
2. Cloud Storageからsitemap.xmlを読み込む
3. updateSitemap関数でsitemap.xmlを返す

![スクリーンショット 2021-01-23 20.49.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398972/6504c261-be96-885f-0031-2fe9c69cb558.png)
##hostingでrewritesする
/sitemap.xmlにアクセスしたらupdateSitemapのFunctionsへとrewriteするようにします。

```javascript

  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**/sitemap.xml",
         "function": "updateSitemap"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
```
注意：実際に静的なsitemap.xmlを置いてしまっているとそちらが優先されてしまいrewriteが作動しないです。
動的sitemapにするときは、静的なsitemapは削除するか、静的なsitemapと動的なsitemapの名前を別にしてください。
参考：https://firebase.google.com/docs/hosting/full-config#hosting_priority_order
1. Reserved namespaces that begin with a /__/* path segment
2. Configured redirects
3. Exact-match static content ← (rewritesより優先度高い）
4. Configured rewrites

## Cloud Storageからsitemap.xmlをDownload
Cloud Storageに保存しているsitemap.xmlをCloud Functionsで取得します。
ここで注意しないといけない点はCloud functionsでは、通常のstorage API使えないのでfirebase admin用の<a href="https://googleapis.dev/nodejs/storage/latest/" target="_blank">cloud storage API</a>を参照する必要があります。

```javascript
// 通常の書き方　← functionsで使えない
const storageRef = admin.storage().ref().child(filePath)
// adminでの書き方　←今回はこっち
const storageRef = admin.storage().bucket().file(filePath)
```

##StorageからDLしてsitemapを返す
StorageからDLしてきたSitemapをString型に変換してクローラ用に返します。ここは特にハマるところはないと思います。

```javascript
  const sitemap = data.toString()
  return res.status(200).send(sitemap)
```

## updateSitemapのFunctionsを全体
全体としてはこんな感じになります。クローラが/sitemap.xmlにアクセスすると、updateSitemapに飛ばされて、updateSitemapでStorageからsitemap.xmlをDLしてきてクローラに返すっという流れになります。

```javascript
import * as functions from 'firebase-functions'
import * as admin from "firebase-admin";
const storage = admin.storage()
/**
 * updateSitemap:
 *   /static/sitemap.xml 以下にアクセスした時に発火する関数
 *   静的なsitemap.xmlがあった場合、hostingはrewritesよりstatic contentsを優先するので注意
 *   firestorageから最新のsitemapファイルを入手してbotに食わす
 *
 */
export default functions.https.onRequest(async (req: any, res: any) => {
  // File pathの指定
  storageRef =  storage.bucket('test.appspot.com').file('sitemap/sitemap.xml')
  // dataの取得
  const data = await storageRef.download()
  if (!data) { return }
  // dataをStingに変換
  const sitemap = data.toString()
  if (!sitemap) { return }
  return res.status(200).send(sitemap)
})
```

#まとめ：動的なSitemapの生成
Cloud FunctionsとCloud Storageで動的なSitemapを作りました。動的なページを本格的に作るならnuxt.jsなどのバックエンドフレームワークを入れるべきかもしれませんが、手軽にやりたいという人にはおすすめです。

https://qnqtree.com/tree/c1QRc2SUPl5jMDVfxKqY
QnQというサービスで使ってますので、よろしければみてみてください。上はJavaScriptのQQトレンドまとめ記事