Flamelink + Nuxt 触ってみた

## Flamelinkとは

Flamelinkとは、FirebaseをヘッドレスCMS化できるサービスです

[Flamelink公式](https://flamelink.io/)

## 参考

- [今すぐ始められる！FIrebaseをブログのCMSに変える「Flamelink」を使ってみた！(2018-11-21)](https://paiza.hatenablog.com/entry/2018/11/21/%E4%BB%8A%E3%81%99%E3%81%90%E5%A7%8B%E3%82%81%E3%82%89%E3%82%8C%E3%82%8B%EF%BC%81FIrebase%E3%82%92%E3%83%96%E3%83%AD%E3%82%B0%E3%81%AECMS%E3%81%AB%E5%A4%89%E3%81%88%E3%82%8B%E3%80%8CFlamelink%E3%80%8D)
- [FlamelinkのクライアントをNuxtで作成してみる(2019-01-13)](https://qiita.com/nunnally_engr_0114/items/68016a821717f015beb2)
- [Flamelink公式のNuxt環境構築サンプル(Github)](https://github.com/flamelink/examples/tree/master/examples/nuxt-basic)

基本的には上記の内容に則って進めていきます
また、参考サイトではDatabaseにRealtime Databaseを使用していますが、今回はCloud Firestoreを使用しています

## もくじ

- [FirebaseとFlamelinkの連携](#flamelinkとfirebaseの連携)
- [Flamelinkで記事の作成](#flamelinkで記事の作成)
- [FlamelinkSDKを利用してNuxtで記事取得](#flamelinksdkを利用してnuxtで記事取得)
- [取得した記事をVuexで管理する](#取得した記事をvuexで管理する)
- [画面の作成](#画面の作成)

## バージョン

```.sh
$ node --version
v10.22.0

$ npm --version
6.14.6
```

## FlamelinkとFirebaseの連携

※ここではFirebaseとFlamelinkを行ったり来たりするので、
先頭に\<Firebase\>、\<Flamelink\>を入れています

### 1. \<Firebase\> Firebaseでプロジェクトを作成しておく

この記事では割愛します

### 2. \<Flamelink\>Flamelinkのアカウント作成

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/94fc3f08-ee0a-1fa4-9e1e-26a2e613eb63.png)


### 3. \<Flamelink\>Databaseの選択

今回はCloud Firestoreを選択
Firestore側のセキュリティルールが表示されているのでコピーしておきます

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/1d776f16-074d-8a0e-9cb1-5722a3b27310.png)


### 4. \<Firebase\> Firestoreのルールを設定

Firebase > Cloud Firestore > ルールに、
先ほどコピーしたルールを貼り付けて公開します

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/4aa70a8d-4309-60b9-fc33-d93a4d9b41b5.png)


### 5. \<Flamelink\> Storageのルールを表示

Databaseのルールと同様にコピー

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/d7267cf7-491b-9dd1-f273-09be3bc2631c.png)


### 7. \<Firestore\> Storageのルールを設定

同様にFirebase > Storage > ルールに貼り付け

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/a66e54de-ec2b-22fb-a338-a33594f7b88f.png)


### 8. \<Flamelink\> 認証の設定

Email/Passwordでの認証を有効にしてと出るので

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/1f2163f4-39ec-c603-df03-a22885a9072f.png)


### 9. \<Firebase\> Authenticationの設定

Firebase > Authentication > Sign-in methodから、
メール/パスワードを有効にします

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/8527dc0a-3ff3-6e12-5d19-9a5545a51703.png)


### 10. \<Flamelink\> 料金プランを選択してFINISH

今回はテストなので無料のFlameを選択、確認して完了！

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/cec16419-63be-0087-1f2e-332880746499.png)
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/f47eeab3-6989-81af-9f9b-9163d9253f10.png)
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/a8c21392-0f4e-ac64-b6d5-ce9d9cada366.png)

## Flamelinkで記事の作成

### 1. PROJECTにログイン

初回のみパスワード設定画面が開きます

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/7783f815-0a96-f931-fdd0-06b558bc4c14.png)


### 2. スキーマを作成

「NEW SCHEMA」からスキーマを作成

![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/d1231872-7d73-084b-2801-127d3907fbb2.png)


任意のtitle, schema IDを設定
ここで指定されたschema IDが、後程Nuxtで記事を取得する際のIDになります

![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/b2be7e49-6187-8b8b-6e02-57e3b787b723.png)


「ADD FIELDS」タブから、記事のフィールドをドラッグ＆ドロップで自由にカスタマイズできます

![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/34cdf9bd-6738-f0ec-439d-1cc2d741175b.png)


完了したら「SAVE」からスキーマを保存

### 3. 記事の登録

左ナビのContentメニューをクリックすると登録したスキーマがでるので「VIEW」をクリック

![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/6f3271e7-d5d3-18c8-b1c9-0f32930da62b.png)


「NEW ENTRY」から記事を投稿
ここでは3件ほど登録しました

![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/7d49170e-717c-3bbe-383c-a47294d147b1.png)


Firebase > Cloud firestoreを確認すると、
Flamelinkから登録したデータが追加されていることが分かると思います

「fl_」の接頭辞がついているコレクションがFlamelinkから追加されたものです
「fl_content」コレクションが記事のデータになります

![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/7a740adf-925f-c517-57fe-566374d8ec22.png)


## FlamelinkSDKを利用してNuxtで記事取得

### 1. Nuxtでプロジェクト作成

```.sh
npx create-nuxt-app <project-name>
```

一度起動して動作を確認

```.sh
npm run dev
```

### 2. 必要なライブラリのインストール

ライブラリをインストールするために、package.jsonに以下を追記します

```package.json.diff
  "dependencies": {
    "@nuxt/typescript-runtime": "^1.0.0",
    "@nuxtjs/axios": "^5.12.0",
    "nuxt": "^2.14.0",
+   "firebase": "^7.19.1",
+   "firebase-admin": "^9.1.1",
+   "flamelink": "^1.0.0-alpha.24"
  },
```

```.sh
npm i
```

でインストール

### 3. NuxtでFlamelinkを使う設定

#### `nuxt.config.js` に以下を追記

```nuxt.config.js
  plugins: [
    '@/plugins/flamelink'
  ],
```

#### flamelink.jsを作成

公式の[flamelink.js](https://github.com/flamelink/examples/blob/master/examples/nuxt-basic/plugins/flamelink.js)をコピーしてプロジェクト直下に保存します
公式はDBにRealtimeDatabaseを使っていたり、Node8を使用していたり、今回の環境と異なる点があるので一部修正します

```flamelink.js.diff
import flamelink from 'flamelink/app'
// This example uses RTDB (Realtime Database) - replace with `cf` for Cloud Firestore
- import 'flamelink/rtdb/content'
- import 'flamelink/rtdb/storage'
+ // 今回Cloud firestoreを使用するのでimportするパッケージを修正
+ import 'flamelink/cf/content'
+ import 'flamelink/cf/storage'
// import 'flamelink/rtdb/settings'
// import 'flamelink/rtdb/navigation'
// import 'flamelink/rtdb/users'

export default ({ app }) => {
  let firebaseApp

  if (process.server) {
    const admin = require ('firebase-admin')

    if (!admin.apps.length) {
-     const serviceAccount = require(process.env.FLAMELINK_PATH_TO_SERVICE_ACCOUNT)
+     // Node10でうまくいかないっぽかったので修正
+     const serviceAccount = require('../serviceAccountKey.json')

      firebaseApp = admin.initializeApp({
        credential: admin.credential.cert(serviceAccount),
        databaseURL: process.env.FLAMELINK_DB_URL,
        storageBucket: process.env.FLAMELINK_STORAGE_BUCKET
      });
    } else {
      firebaseApp = admin.app()
    }
  } else {
    const firebase = require('firebase/app')
    // require('firebase/auth')
    // require('firebase/firestore')
    require('firebase/database')
    require('firebase/storage')

    if (!firebase.apps.length) {
      firebaseApp = firebase.initializeApp({
        apiKey: process.env.FLAMELINK_API_KEY,
        authDomain: process.env.FLAMELINK_AUTH_DOMAIN,
        databaseURL: process.env.FLAMELINK_DB_URL,
        projectId: process.env.FLAMELINK_PROJECT_ID,
        storageBucket: process.env.FLAMELINK_STORAGE_BUCKET,
        messagingSenderId: process.env.FLAMELINK_MESSAGING_SENDER_ID
      })
    } else {
      firebaseApp = firebase.app()
    }
  }

+ //dbTypeをcf(Cloud firestore)に変更
- app.flamelink = flamelink({ firebaseApp, dbType: 'rtdb' })
+ app.flamelink = flamelink({ firebaseApp, dbType: 'cf' })
}
```

### 4. Firebase側の設定

#### サービスアカウントのjsonファイルを作成して設置

サービスアカウント情報を含むJsonファイルをFirebase側で生成、ダウンロードし、
`serviceAccountKey.json` にリネームしてNuxtのプロジェクト配下に設置します。
Jsonファイルは `設定 > サービスアカウント > 新しい秘密鍵の生成` で作成できます

![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/a54d857f-6f47-f8b8-146d-cacb9330e0fc.png)

#### .gitignoreにserviceAccountKey.jsonを追加

```.gitignore.diff
+ # Firebase service account key
+ serviceAccountKey.json
```

#### .envファイルの作成

プロジェクト直下に `.env` ファイルを作成し、以下の情報を埋めます
Firebase > 設定 > 全般から取得できます

```.env
FLAMELINK_API_KEY="<firebase-api-key>"
FLAMELINK_AUTH_DOMAIN="<firebase-auth-domain>"
FLAMELINK_DB_URL="<firebase-db-url>"
FLAMELINK_PROJECT_ID="<firebase-project-id>"
FLAMELINK_STORAGE_BUCKET="<firebase-storage-bucket>"
```

![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/b72f4d10-e236-9b20-a3f4-f27b1b67cc33.png)


#### 最終的なディレクトリ構造

![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/b524832d-2a8f-583d-40b9-92246451e6c6.png)

### 5. コンポーネントから記事データを取得できるか確認

`pages/index.vue` で取得テスト

```index.vue.diff
export default {
  components: {
    Logo,
    VuetifyLogo,
  },

+ async asyncData({ app }) {
+   try {
+     const myposts = await app.flamelink.content.get({
+       schemaKey: 'myposts', // Flamelinkで設定したschemeID
+       populate: true
+     })
+     console.log({ myposts })
+     return { myposts }
+   } catch (err) {
+     console.log(err)
+     return { myposts: [] }
+   }
+ }
+
```

```.sh
npm run dev
```

ブラウザのコンソールで記事情報が取得できているかを確認（Vueの開発ツールでもOK）

![21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/09800c87-2f8f-4b83-222c-76a5dfcd757c.png)

**無事取得できていました :tada:**

## 取得した記事をVuexで管理する

Flamelinkから取得した記事のデータは一覧ページと詳細ページで共通なので、Vuexでストア管理することにした
先ほどの `index.vue` に記述した取得ロジックをstoreに移植する

### storeの準備

`store/index.ts` を作成し、`state`, `getters`, `mutations` を用意

[＜参考＞Nuxt-Vuexの型定義](https://typescript.nuxtjs.org/ja/cookbook/store/#vanilla)

```store/index.ts
import { GetterTree, ActionTree, MutationTree } from 'vuex'

import { formatDate } from '~/utils/format' // *これは自前で用意した関数
import { PostData } from '~/types' // *これは自前で用意した型

export type RootState = ReturnType<typeof state>

export const state = (): {
  posts: PostData[] | null
} => ({
  posts: null
})

export const getters: GetterTree<RootState, RootState> = {
  posts: state => state.posts
}

export const mutations: MutationTree<RootState> = {
  FETCH_POST: (state, payload: PostData[]) => {
    state.posts = payload
  }
}
```

### nuxtServerInitで記事の取得

[＜参考＞nuxtServerInitアクション](https://url.fork.jp/vFZz)

> **nuxtServerInit** というアクションがストア内に定義されて、かつ **universal** モードである場合は、Nuxt.js はそれをコンテキストとともに呼び出します（ただしサーバーサイドに限ります）。サーバーサイドからクライアントサイドに直接渡したいデータがあるときに便利です。

`store/index.ts` にアクションを追加

[Flamelink JS SDK](https://url.fork.jp/d27H)の理解が少し足りず力技の部分もあるかもなのだが、
以下のとおり取得データを加工している

- 1. **ソート用に記事作成日時データを追加**  
今回、`date` にFlamelinkから作成日のデータが注入されるのだが、 `"2020-09-09T00:00:00+09:00"` のように、同日付の記事は時間の部分が固定になっていた  
そのため同日作成の記事をうまくソートできないので、 `_fl_meta_.createdDate._seconds` から日時を取得し、`createdDate` フィールドに設定
- 2. **フォーマット化した日付を用意**  
1の日時データをもとにフォーマット化した日付文字列を `formatedDate` フィールドに設定
- 3. **記事のソート**  
1の日時データをもとに記事データを降順にソート

```store/index.ts
export const actions: ActionTree<RootState, RootState> = {
  async nuxtServerInit ({ commit }, { app }) {
    const getData: {
      [key: string]: PostData
    } = await app.flamelink.content.get({
      schemaKey: 'myposts',
      populate: true
    })

    // データ加工
    const myposts = Object.entries(getData)
      .map(([k, v]) => {
        const createdDate = new Date(v._fl_meta_.createdDate._seconds * 1000)
        v.createdDate = createdDate // (1)
        v.formatedDate = formatDate(createdDate) // (2)
        return v
      })
      .sort((a: PostData, b: PostData) => a.createdDate > b.createdDate ? -1 : 1) // (3)

    commit('FETCH_POST', myposts)
  }
}
```

**無事Vuexにぶち込まれました**:tada:

![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/b2700682-a6e6-b0e4-4a9e-96ae9d4a6423.png)

## 画面の作成

### 記事一覧

`pages/index.vue`

- データ `myposts` に、ストアで用意したゲッター経由でデータを取得して格納
- `myposts` を `v-for` でループ
- リンクパラメータには `id` （記事ID）をセット

※ `BlogList.vue` は日付と見出しを受け取って表示するだけのコンポーネント

```pages/index.vue
<template lang="pug">
  v-layout(column justify-center align-center)
    v-row
      v-col(v-for="item in myposts" :key="item.id" cols="12")
        nuxt-link(:to="`/article/${item.id}`")
          blog-list(
            :title="item.title"
            :date="item.formatedDate"
          )
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import BlogList from '~/components/BlogList.vue'

@Component

export default class PageIndex extends Vue {
  myposts = this.$store.getters.posts
}
</script>
```

### 記事詳細

`pages/article/_id.vue`

※ 動的ルーティングさせるのでファイル名をスネークケースに

- `index.vue` 同様に記事データをストアから取得
- `this.$route.params.id` にわたってくる記事IDで該当記事データをフィルター

```pages/article/_id.vue
<template lang="pug">
  div
    h1
      | {{ entry.title }}
    div(v-html="entry.content")
</template>

<script lang="ts">
import {Component, Vue} from 'nuxt-property-decorator'
import { PostData } from '~/types'

@Component
export default class PageArticle extends Vue {
  myposts = this.$store.getters.posts

  get entry() {
    const id = this.$route.params.id
    const entry = this.myposts.find((e: PostData) => {
      return e.id === id
    })
    return entry
  }
}
</script>
```

**GOOD JOB** :tada:

![25.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315402/2aaff6ee-f151-5827-1cc4-da2057f29291.gif)

## 雑感
記事の投稿フォーマットからFirestoreへの登録までFlamelinkが担ってくれるので、かなり簡単にブログが実装できるのを感じました

とりあえず今回は記事の取得、表示までを実装してみましたが、現段階ではブログ更新の都度 `nuxt generate` して `firebsae deploy` でホストさせる仕組みにとどまっています
引き続きJamstackで静的配信できるよう勉強していきたいと思います

---
[:christmas_tree: FORK Advent Calendar 2020](/advent-calendar/2020/fork)
:arrow_left: 14日目 [GCS へのファイルアップロードをトリガーにBigQueryにデータをインポートする。](https://qiita.com/shuhei4009/items/74619946d60665daedf8) @shuhei4009
:arrow_right: 16日目 [Full Static Generationを試す](https://qiita.com/AsaToBan/items/a2f172c45ca12e5e52dc) @AsaToBan