JavaScript
vue.js
GraphQL
apollo
HeadlessCMS

HeadLessCMS + GraphQL + Vue.js でSPA(ブログ)を作ってみる

新しめの技術を勉強したいと思い、HeadLessCMS + GraphQL + Vue.js で簡単なSPA(ブログ)を作ってみました。
その過程で学んだことをチュートリアル形式でまとめます。
なるべく初学者の方にもわかりやすいようスクリーンショットを多めに掲載しています。

概要

headlessCMS、GraphQL、Vue.js、Apolloでブログを作成します。
チュートリアルがメインなので、graphQLのクエリの詳細な説明などは省略しています。
以下各バージョンです。

  • GraphCMS: 2018.7公開のもの
  • Vue cli: 3.0.1
  • vue: 2.5.17
  • vue-apollo: 3.0.0-beta.11
  • vue-router": 3.0.1
  • vuetify: 1.2.4
  • graphql: 14.0.2

作成サンプル

このようなのトップページ(投稿一覧)と詳細ページの2種類のページを持つブログ(SPA)を作成します。
見栄えはvueのコンポーネントライブラリのVuetifyでそれっぽく頑張っています。

output1.gif

herokuにデプロイしているものはこちらから確認できます。
(常に起こしているわけではないので、初回アクセスは起動が遅いです)
https://graphcms-sampleblog.herokuapp.com/

ソースはこちらです。
https://github.com/kawamataryo/vue-sample-blog

APIサーバーの作成

まず、headressCMSのGraphCMSを使ってAPIサーバーを作成します。
CMSという名の通り、プラウザでポチポチとやっていたらインフラ、DBの構築、APIの設計等々なにも考える必要なく、
APIサーバーをつくることが出来ます。しかもレスポンスはGraphQLです。

GraphCMSアカウント作成

GraphCMSホームページからSignUpでアカウントを作成します。
github、facebook等でのsignupも出来ます。
次にプロジェクトを作成します。
今回は、sampleというプロジェクトを作成しました。
スクリーンショット 2018-09-16 15.37.30.png

プランの選択がありますが、無料で大丈夫です。
スクリーンショット 2018-09-16 15.37.44.png

スキーマの作成

サイドメニューのschemeよりModelを作成します。
DBのようなものです。これがAPIのデータにひも付きます。
Model内にwordpressのカスタム投稿タイプのように、inputフィールドを作成することができます。

スクリーンショット 2018-09-16 15.39.25.png

右上のCreateModelより作成できます。
今回は以下内容で作成しました。

# モデルの構成
displayName: post
api key: Post
# フィールドの構成
title: single line text
description: multi type text
content: mark down
thumbnail: asset picker

スクリーンショット 2018-09-16 15.40.54.png

データの追加

サイドメニューのcontent > posts を選択し、右上のcreate Postよりデータの追加が行えます。
なにか適当に追加しておきましょう。
こちらのデータを後から説明するvue-apolloで取得します。

スクリーンショット 2018-09-16 20.47.14.png

GraphQLを試してみる

コンテンツを入力したら、早速GraphQLを試してみましょう。
サイドメニューのAPI Exploereより、GraphQLのリクエストが試せます。
入力のオートコンプリートも効きますし、右側のDocsという小さいボタンをクリックすると、
作成したModelに合わせた、GraphQLのquery情報が確認できます。

スクリーンショット 2018-09-17 23.35.30.png

API情報の確認

データの取得に使用するエンドポイントは、サイドメニューのsettingsより確認できます。
GraphQLなので、ワンラインです。
その他、settignsではエンドポイントの公開設定等行えます。
初期はreadOnlyです。今回は特に変更せずとも大丈夫です。

スクリーンショット 2018-09-16 15.41.49.png

フロントエンド開発

つづいてgraphCMSで作成したデータを表示するフロントエンドを作成します。
vueプロジェクトを簡単に作成できるvue cliを使用します。

プロジェクトの作成

まず、vue cliのインストール。

$ npm install -g @vue/cli-service-global

そしてvue cli3でプロジェクトを作成します。

$ vue create sample-blog

選択肢はデフォルトでOKです。
以下コマンドで起動し、
http://localhost:8088/にアクセスして無事起動できればプロジェクト作成OKです。

$ cd sample-blog
$ yarn serve

スクリーンショット 2018-09-23 7.06.43.png

vuetifyの追加

モックでも見栄えが良いほうがやる気がでるので、最初にvue.jsのコンポーネントライブラリのvuetifyを追加します。
追加するとマテリアルデザインの様々なコンポーネントが使えるようになります。
vuetifyはマニュアルも豊富で、一部日本語翻訳されています。

インストール

vue cli3 ではvuetifyをpluginとして簡単に追加できます。

$ vue add vuetify

するとオプションの選択肢が出てきます。
今回は以下のように設定しました。

? Use a pre-made template? (will replace App.vue and HelloWorld.vue) Yes
? Use custom theme? No
? Use custom properties (CSS variables)? No
? Select icon font fa4
? Use fonts as a dependency (for Electron or offline)? No
? Use a-la-carte components? No
? Use babel/polyfill? Yes
? Select locale en

それでは以下コマンドで起動してみます。

$ yarn serve

http://localhost:8080/
にアクセスして、無事下記画面が表示されれば、vuetifyのインストール完了です。

スクリーンショット 2018-09-23 7.27.58.png

補足 yarn serveでエラーが発生した場合

私の環境だと以下エラーが発生しました。

 ERROR  Failed to compile with 1 errors                                                                                                                                           07:22:42

 error  in ./src/main.js

Module build failed (from ./node_modules/babel-loader/lib/index.js):
BrowserslistError: [BABEL] /Users/kawamataryou/vue_training/sample-blog/src/main.js: /Users/kawamataryou/vue_training/sample-blog contains both .browserslistrc and package.json with brow
sers (While processing: "/Users/kawamataryou/vue_training/sample-blog/node_modules/@vue/babel-preset-app/index.js$0")

contains both .browserslistrc and package.json with browとの通り、package.jsonと、.browserslistrcが両方あることが問題のようなので、.browserslistrcを削除します。

$ rm .browserslistrc

そして再起動すれば解消されました。

サンプルコンポーネントを作成

早速vuetifyを使って記事一覧表示用のCardコンポーネントを作成します。

$ touch src/components/PostCard.vue   

src/components/PostCard.vue に以下を記載します。

src/components/PostCard.vue
<template>
  <v-flex xs12 sm6 md4>
    <article>
      <v-hover>
        <v-card
            slot-scope="{ hover }"
            :class="`elevation-${hover ? 12 : 2}`"
        >
          <a href="">
            <v-img
                class="white--text"
                height="170px"
                :src=post.thumbnail.url
            >
            </v-img>
          </a>
          <v-card-title>
            <div>
              <h2>{{ post.title }}</h2>
              <span class="grey--text">{{ post.createdAt}}</span><br>
              <span>{{ post.description }}</span>
            </div>
          </v-card-title>
          <v-card-actions>
            <v-btn icon class="red--text">
              <v-icon medium>fa-reddit</v-icon>
            </v-btn>
            <v-btn icon class="light-blue--text">
              <v-icon medium>fa-twitter</v-icon>
            </v-btn>
            <v-btn icon class="blue--text text--darken-4">
              <v-icon medium>fa-facebook</v-icon>
            </v-btn>
            <v-spacer></v-spacer>
            <v-btn flat color="blue" href="">Read more</v-btn>
          </v-card-actions>
        </v-card>
      </v-hover>
    </article>
  </v-flex>
</template>

<script>

  export default {
    name: "PostCard",
    props: ["post"]
  }
</script>

そして、このコンポーネントを読み込むようにApp.vueを修正します。

src/App.vue
<template>
  <v-app>
    <v-container grid-list-xl>
      <v-layout row wrap>
        <PostCard
            v-for="post in posts"
            v-bind:key="post.id"
            :post=post
        ></PostCard>
      </v-layout>
    </v-container>
  </v-app>
</template>

<script>
  import PostCard from './components/PostCard'

  export default {
    name: 'App',
    components: {
      PostCard,
    },
    data: () => ({
      posts: [
        {
          id: 1,
          createdAt: "2018/09/23",
          title: "sample post",
          description: "sample post description",
          contents: "sample post contents",
          thumbnail: {
            url: "https://picsum.photos/800/400?image=80"
          }
        },
        {
          id: 2,
          createdAt: "2018/09/24",
          title: "sample post2",
          description: "sample post description2",
          contents: "sample post contents2",
          thumbnail: {
            url: "https://picsum.photos/800/400?image=90"
          }
        },
        {
          id: 3,
          createdAt: "2018/09/24",
          title: "sample post3",
          description: "sample post description3",
          contents: "sample post contents3",
          thumbnail: {
            url: "https://picsum.photos/800/400?image=100"
          }
        },
      ]
    })
  }
</script>

この時点でyarn serveを行い、以下のように表示されればOKです。
以降このコンポーネントをもとにデータの追加などを行っていきます。

スクリーンショット 2018-09-23 8.05.12.png

APIクライアント Apolloの追加

ApolloはGraphQLのクライントライブラリです。
Apolloを使ってHeadlessCMSからデータを取得してみます。

vue-apolloのインストールと設定

インストールはvue cliを使います。
その他のインストール方法はこちらで説明されています。

$  vue add apollo       

オプションはすべてデフォルトで大丈夫です。
自動的にsrc/以下にapolloの設定ファイルvue-apollo.jsが作成され、
main.jsに読み込み設定が追加されます。

src/vue-apollo.js
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'

// Install the vue plugin
Vue.use(VueApollo)

// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'

// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql'

// Config
const defaultOptions = {
  // You can use `https` for secure connection (recommended in production)
  httpEndpoint,
  // You can use `wss` for secure connection (recommended in production)
  // Use `null` to disable subscriptions
  wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
  // LocalStorage token
  tokenName: AUTH_TOKEN,
  // Enable Automatic Query persisting with Apollo Engine
  persisting: false,
  // Use websockets for everything (no HTTP)
  // You need to pass a `wsEndpoint` for this to work
  websocketsOnly: false,
  // Is being rendered on the server?
  ssr: false,
.
.
.
src/main.js
import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
+ import { createProvider } from './vue-apollo'

Vue.config.productionTip = false

new Vue({
+  apolloProvider: createProvider(),
  render: h => h(App)
}).$mount('#app')

エンドポイントの設定を追加で行います。
src/vue-apollo.jsをのEndpointの設定欄を見ると、、

src/vue-apollo.js
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/graphql'

となっています。これは、エンドポイントとして環境変数のVUE_APP_GRAPHQL_HTTPを参照し、
それが設定されて無ければ'http://localhost:4000/graphql'をエンドポイントとするという意味です。
なので、vue cliの環境変数にVUE_APP_GRAPHQL_HTTPを追加して、そこにGraphCMSのエンドポイントURLを設定しましょう。
また、wsEndpointについては使用しないので、nullを設定します。
環境変数の設定についてはvue clid3ドキュメントのこちらに記載されています。

.envファイルを作成

# プロジェクトルートで、、
$ vi .env

.envに以下を追記します。graphCMSのエンドポイントは前述のsettingsからコピーしたものを使ってください。

.env
VUE_APP_GRAPHQL_HTTP=https://api-apeast.graphcms.com/xxxxxxxxx/master

そしてwssは今回使用しないので、
以下オプションはapollo.jsの以下オプションは削除してください。

.
.
const defaultOptions = {
  // You can use `https` for secure connection (recommended in production)
  httpEndpoint,
-  // You can use `wss` for secure connection (recommended in production)
-  // Use `null` to disable subscriptions
-  wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
  // LocalStorage token
  tokenName: AUTH_TOKEN,
.
.

これでApolloの設定は終了です。

GraphQLの追加

やっとGraphQLの出番です。ここから、GraphQLのクエリを書いていきます。
まず、クエリを記述するgraphql.jsファイルの追加です。

$ mkdir src/constants
$ touch src/constants/graphql.js

graphql.jsにクエリを書いていきます。
最初はpostの全件取得です。

src/constants/graphql.js
import gql from 'graphql-tag'

// すべての投稿を取得
export const ALL_POSTS = gql`
    query allPosts {
        posts {
            id
            title
            content
            description
            createdAt
            thumbnail {
                url
            }
        }
    }
`

作成したクエリをApp.vueで読み込みます。
そして、dataで直書きしていた部分をApolloでの取得結果に置き換えます。
apollo: 以下に先程宣言したクエリを追加するだけです。

src/App.vue
.
.
.

<script>
  import PostCard from './components/PostCard'
+ import {ALL_POSTS} from "./constants/graphql";

  export default {
    name: 'App',
    components: {
      PostCard,
    },
+    apollo: {
+      posts: ALL_POSTS
+    }
-    data: () => ({
-      posts: [
-        {
-          id: 1,
-          createdAt: "2018/09/23",
-          title: "sample post",
-          description: "sample post description",
-          contents: "sample post contents",
-          thumbnail: {
-            url: "https://picsum.photos/800/400?image=80"
-          }
-        },
-        {
-          id: 2,
-          createdAt: "2018/09/24",
-          title: "sample post2",
-          description: "sample post description2",
-          contents: "sample post contents2",
-          thumbnail: {
-            url: "https://picsum.photos/800/400?image=90"
-          }
-        },
-        {
-          id: 3,
-          createdAt: "2018/09/24",
-          title: "sample post3",
-          description: "sample post description3",
-          contents: "sample post contents3",
-          thumbnail: {
-            url: "https://picsum.photos/800/400?image=100"
-          }
-        },
-      ]
-    })
  }
</script>

これで、yarn serveで起動するとGraphCMSで追加した投稿データが表示されるはずです。

eoutput2.gif

ルーティングの追加 vue-router

SPAということで、一覧ページから詳細ページへのルーティングを追加します。
ルーティングにはvue-routerを使用します。

インストールと設定

vue-routerは、yarnでインストールします。
(vue cliのaddだとデフォルトファイルの追加や、App.vueの書き換えが自動で行われ面倒なので、、)

$ yarn add vue-router

次に設定ファイルを追加します。

$ touch src/router.js

router.jsにルーティングを記述します。
ここで宣言してるPostList.vueとPostDetail.vueは後ほど追加します。

src/router.js
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

// ルーティング
const routes = [
  {
    // 記事一覧ページ
    path: '/',
    name: "postList",
    component: () => import('./views/PostList.vue')
  },
  {
   // 記事詳細ページ
    path: '/post/:id',
    name: "postDetail",
    component: () => import('./views/PostDetail.vue')
  },
]

const router = new VueRouter({
  routes: routes,
  base: process.env.BASE_URL,
  mode: 'history',
  // ページ遷移の際の位置指定 指定がない場合 ページトップへ
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return {x: 0, y: 0}
    }
  }
});

export default router;

次にこれをmain.jsで読み込むよう設定を追記します。

src/main.js
import '@babel/polyfill'
import Vue from 'vue'
import './plugins/vuetify'
import App from './App.vue'
import { createProvider } from './vue-apollo'
+ import router from "./router"

Vue.config.productionTip = false

new Vue({
  apolloProvider: createProvider(),
+  router,
  render: h => h(App)
}).$mount('#app')

最後に、App.vueの一覧ページのコンポーネントを記載していた部分をvue-routerの遷移タグ に変更します。

src/App.vue
<template>
  <v-app>
    <transition name="fade">
      <router-view/>
    </transition>
  </v-app>
</template>

<script>
  export default {
    name: 'App',
  }
</script>

ここまででrouterの設定は完了です。

vueコンポーネントの追加

次にページ遷移先のvueコンポーネントを設定していきます。
一覧ページと詳細ページのコンポーネントを作成します。

一覧ページは、App.vueに記載していたものを移して作成します。

src/views/PostList.vue
<template>
  <v-container grid-list-xl>
    <v-layout row wrap>
      <PostCard
          v-for="post in posts"
          v-bind:key="post.id"
          :post=post
      ></PostCard>
    </v-layout>
  </v-container>
</template>

<script>
  import PostCard from '../components/PostCard'
  import {ALL_POSTS} from "../constants/graphql";

  export default {
    name: "PostList",
    components: {
      PostCard,
    },
    data: () => ({
      posts: [],
    }),
    apollo: {
      posts: ALL_POSTS
    }

  }
</script>

詳細ページは、以下のような構成にします。
まだ、postがdataに直書きですが、後からapolloでの取得に切り替えます。

src/views/PostList.vue
<template>
  <div>
    <section>
      <v-parallax :src="post.thumbnail.url" height="600">
        <v-layout
            column
            align-center
            justify-center
            class="white--text"
        >
          <h1 class="white--text mb-2 display-2 text-xs-center">{{post.title}}</h1>
          <p class="white-text text-xs-center subheading">{{ post.description }}</p>
        </v-layout>
      </v-parallax>
    </section>
    <v-content>
      <v-container>
        <v-layout row wrap justify-center>
          <v-flex xs12 md8>
            <p>{{ post.content }}</p>
          </v-flex>
        </v-layout>
      </v-container>
    </v-content>
  </div>
</template>

<script>

  export default {
    name: 'PostDetail',
    data: () => ({
      post: {
        title: "sample title",
        description: "sample description",
        content: "amet, consectetur adipisicing elit. Culpa debitis dolores eius facilis officiis quo unde velit. Ab accusantium aperiam commodi cupiditate dignissimos eius eum sequi sunt tempore, vero voluptas.Consequuntur deleniti doloremque eius incidunt modi non repellendus sapiente ut vel. Autem neque, ullam. Atque aut eveniet, exercitationem illo illum inventore molestias numquam, optio quas recusandae, repellendus suscipit tenetur vero?Blanditiis consequuntur deserunt dolor ducimus modi necessitatibus, odit placeat quaerat quia saepe sequi unde ut voluptate? Aspernatur consectetur, dignissimos eaque fuga in laborum odit, quibusdam rem rerum sed sit soluta?Aliquam atque deleniti dolorem laborum maxime voluptates? A architecto, corporis earum explicabo fugiat labore porro velit! Adipisci at consequatur, error eveniet, laboriosam minus nemo nihil numquam quisquam rerum sequi temporibus.Deleniti, fuga, quibusdam! Aspernatur commodi cum doloremque esse est eum illo inventore ipsum itaque laborum molestias mollitia nostrum, odio officiis omnis perspiciatis possimus quia quidem recusandae tempore vero voluptate voluptates.",
        thumbnail: {
          url: "https://picsum.photos/1200/600?image=90"
        }
      }
    })
  }
</script>

<style>
  .v-parallax__image {
    opacity: 1!important;
  }
</style>

http://localhost:8080/ で今までの一覧ページ。
http://localhost:8080/post/1 で以下詳細ページが表示されればOKです。

スクリーンショット 2018-09-24 0.13.09.png

詳細ページのデータをApolloで取得

前項でコンポーネントに直書きしていたpostをApolloでの取得に切り替えます。

まず、IDで記事を1件取得するクエリをgraphql.jsに追記します。

src/constants/graphql.js
.
.
// IDで1件取得
export const FEACH_POST_BY_ID = gql`
    query feachPostById($id: ID!) {
        post(where: { id: $id }) {
            title
            content
            description
            createdAt
            thumbnail {
                url
            }
        }
    }
`

次にそれを、PostDetailで読み込みます。
以下を修正します。

src/views/PostDetail.vue
<script>
  import {FEACH_POST_BY_ID} from "../components/graphql";

  export default {
    name: 'PostDetail',
    data: () => ({
      post: [] 
    }),
    apollo: {
      post: {
        query: FEACH_POST_BY_ID,
        variables() {
          return {
            id: this.$route.params.id,
          }
        },
      }
    }
  }
</script>

apollo.post.variables() 以下で、クエリに渡す引数を設定できます。
今回はidとして、this.$route.params.idでvue-routerのurlパラーメーターを設定しています。
localhost:8080/post/xxxのxxxの部分が取得できます。

一覧から詳細へのリンクの追加

最後に一覧から詳細へのリンクを設定します。vuetifyのコンポーネントでは:toでvue-routerのリンクを設定可能です。
一覧のPostCardコンポーネントにリンクを追加します。

src/components/PostCard.vue
  <v-flex xs12 sm6 md4>
    <article>
      <v-hover>
        <v-card
            slot-scope="{ hover }"
            :class="`elevation-${hover ? 12 : 2}`"
        >
+          <a v-bind:href="'/post/' + post.id">
            <v-img
                class="white--text"
                height="170px"
                :src=post.thumbnail.url
            >
            </v-img>
+          </a>
          <v-card-title>
            <div>
              <h2>{{ post.title }}</h2>
              <span class="grey--text">{{ post.createdAt | moment }}</span><br>
              <span>{{ post.description }}</span>
            </div>
          </v-card-title>
          <v-card-actions>
            <v-btn icon class="red--text">
              <v-icon medium>fa-reddit</v-icon>
            </v-btn>
            <v-btn icon class="light-blue--text">
              <v-icon medium>fa-twitter</v-icon>
            </v-btn>
            <v-btn icon class="blue--text text--darken-4">
              <v-icon medium>fa-facebook</v-icon>
            </v-btn>
            <v-spacer></v-spacer>
-            <v-btn flat color="blue">Read more</v-btn>
+            <v-btn flat color="blue" :to="'/post/' + post.id">Read more</v-btn>
          </v-card-actions>
        </v-card>
      </v-hover>
    </article>
  </v-flex>
</template>

以上でルーティングが完成です。
以下のように画面遷移ができるはずです。

output3.gif

ここまででフロントエンドは終了です。
本当はpagingなどもやりたいのですが、かなり長文となってしまっているので、別の機会にまとめたいと思います。

デプロイ

最後に、せっかく作ったプロジェクトなので外部公開してみましょう。
herokuを使えば簡単にvueプロジェクトがデプロイできます。

expressの追加と設定

Node.jsのサーバーサイドJavascriptの実行環境 explessを追加します。

$ yarn add express

そして、サーバーの設定となるserver.jsをプロジェクトルートに追加します。

$ vi server.js
server.js
const express = require('express');
const port = process.env.PORT || 5000;
const app = express();
app.use(express.static(__dirname + "/dist/"));

app.get(/.*/, function(req, res) {
  res.sendfile(__dirname + "/dist/index.html");
});

app.listen(port);

server.jsの書き方については、以下を参考にさせていただきました。
Vue Cli 3のプロジェクトをHerokuにデプロイする

次にpackage.jsonにserverの起動コマンド等を追加します。

package.json{
  "name": "sample-blog",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
+    "postinstall": "yarn build",
+    "start": "node server.js"
  },
.
.
.

この状態で、

$ yarn start

でエラーなくサーバーが起動し http://localhost:5000/ にアクセスして一覧画面が表示されればサーバーの設定は完了です。

helokuへのデプロイ

heloku cliの設定は終わっている前提で進めます。

herokuはgitを介してデプロイを行います。
まず実行ファイルとなるdist/以下のディレクトリがバージョン管理されるように.gitignoreを編集します。

$ vi .gitignore

/distの欄をコメントアウトor削除しましょう。

# /dist

次にgitの初期化とステージング、commitを行います。

$ git init
$ git add .
$ git commit -m "init for heroku"

次にherokuのプロジェクトを作成します。

$ heroku create graphcms-sampleblog   
# graphcms-sampleblogの部分がurlの一部となります。既存と被ると使えないので注意

そして、heroku上にデプロイします。
コマンド一つでデプロイ完了です。

$ git push heroku master

早速サイトを見てみましょう。

$ heroku open

https://プロジェクト名.herokuapp.com/ で無事プラウザで表示されればデプロイ完了です。
あと、vuetifyのnavbarや、footerなどを最初にサンプルで掲載した追加すればデモサイトが出来ます。

感想

いやー。Vue.js楽しい。スピード感が全く違う。GraphQLすごい。クエリがわかりやすい。
コーディング、デザイン、デプロイ、公開までのサイクルが本当に早いと感じました。
こんな素晴らしいOSSを作ってくれている先人に感謝です。

今回、説明できなかったページングや、CRUD(記事追加、編集、削除)についても今後記事を公開したいと思います。
typo、この書き方は非推奨、など情報ありましたら編集リクエストかコメントで教えて頂ければ幸いです。

追記

ページネーションの実装について記事追加しました。
vue-apollo + vuetifyでページネーションを実装してみる(課題あり)