15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FORKAdvent Calendar 2020

Day 15

Flamelink + Nuxt 触ってみた

Last updated at Posted at 2020-12-14

Flamelinkとは

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

Flamelink公式

参考

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

もくじ

バージョン

.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

3. <Flamelink>Databaseの選択

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

2.png

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

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

3.png

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

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

4.png

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

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

5.png

8. <Flamelink> 認証の設定

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

6.png

9. <Firebase> Authenticationの設定

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

7.png

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

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

8.png
9.png
10.png

Flamelinkで記事の作成

1. PROJECTにログイン

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

11.png

2. スキーマを作成

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

12.png

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

13.png

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

14.png

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

3. 記事の登録

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

15.png

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

16.png

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

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

17.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をコピーしてプロジェクト直下に保存します
公式は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

.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

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

20.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

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

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

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

storeの準備

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

<参考>Nuxt-Vuexの型定義

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アクション

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

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

Flamelink JS SDKの理解が少し足りず力技の部分もあるかもなのだが、
以下のとおり取得データを加工している

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

画面の作成

記事一覧

pages/index.vue

  • データ myposts に、ストアで用意したゲッター経由でデータを取得して格納
  • mypostsv-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

雑感

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

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


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 14日目 GCS へのファイルアップロードをトリガーにBigQueryにデータをインポートする。 @shuhei4009
:arrow_right: 16日目 Full Static Generationを試す @AsaToBan

15
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?