What's?

Home / Gridsome(Vue.js)とHeadlessCMS(Contentful)でBlogサイトを作る

Gridsome(Vue.js)とHeadlessCMS(Contentful)でBlogサイトを作る
May 01, 2020

DEVELOPMENT

web
node
Contentful
Vue
Gridsome

はじめに

Vue.jsのプロジェクトがあり、少しは慣れておこうと思い、Vue.jsでショッピングサイトを作ろうと思っていました。

その際、商品の詳細ページをブログのようにMarkdown形式で登録できたらなーと思い、色々と探していました。 特にHeadlessCMSを使い、JAMStackにこだわるつもりはなく、localにMarkdownファイルを置いて作っていければなーと思っていたのですが、調べていくうちに、

Gridsome」を発見しました。

今回はこちらを使い「記事の一覧と記事詳細ページ」の作成までをご紹介します。

GridSomeとは?

Gridsome」は、VuejsのGatsby版みたいなものだそうで、 Gatsbyは、Reactで開発されていて、Reactで開発ができる静的サイトジェネレーターです。 最近はGatsby Cloudも出てきて、Cloud上でBuildやDeployができるというものですね。

あと、Gatsbyと同じようにデータのやり取りはGraphQLで行えるのも魅了です

この辺のHeadlessCMSやGatsby Cloud、静的サイトジェネレータについてはこちらの記事に記載してます

Gridsome」は、そのVuejs版で静的にサイトをジェネレートを出来るのはもちろん、Gatsby Cloudのように、BuildやDeployもできるPlatformだそうです。

ただ、まだGatsbyと比べると機能的に不完全だったりで、導入事例がまだまだ少なそうです。 こちらの記事を参考にさせていただきました。

今回は、こちらの「Gridsome」で、例にちょうどあった、HeadlessCMSの代表のContentfulを使い、JAMStackなWEBサイトの実装例を紹介したいと思います。

なお、こちらに行きつく前にリサーチした方法も一応次に記しておきます。

ButterCMSの例

Vue.jsの公式ページを見ると、ButterCMSの例が掲載されています。

ButterCMSはまだ新しそうなHeadlessCMSですが、Freeトライアル期間が過ぎると有料のプランしかなさそうだったので、今回は諦めました。

NuxtとContentfulの例

更に調べると、今度は、Nuxt.jsと、Contentfulでの実装例がQiitaContentfulの公式などに良く見受けられました。

Nuxtはあまりやったことないのですが、無駄な部分が多そうな気がして、コンパクトに必要最低限に実装したい僕には合わなそうだったので、やめました。

GridsomeとHeadlessCMS(Contentful)を使った実装手順

前置きが長くなりましたが、紹介します。

前提

  • Node 環境くらい
  • ここではYarnを使ってます

Contentfulの登録と設定

こちらの詳しい手順はこちらの記事に記載してますので、参考にしてください。

  • ▶ 参考 Contentfulサインアップと設定
  • サインアップ
  • Model作成
  • Modelのフィールド作成(今回はContentfulのサンプルデータを使ったので、僕の場合BlogPost)
  • フィールド登録(titleやdescription, tags等)
  • BlogPostにContentを登録してPublishしておく
  • SettingのAPIKeysから

    • SpaceID
    • DeliveryのAccessTokenをメモっておく

以上です

Gridsomeでプロジェクトを作成

$ yarn global add @gridsome/cli
  • proj作成
$ gridsome create プロジェクト名
$ cd プロジェクト名
  • 確認

    $ yarn run develop

するとHello Worldが表示されます。

GridsomeにContentfulの設定をする

  • pluginを入れる
$ yarn add @gridsome/source-contentful
  • gridsome.config.js このファイルが基本、Gatsbyでいう、gatsby-config.jsと同じような感じです。こちらに以下を追記
module.exports = {
  plugins: [
    {
      use: '@gridsome/source-contentful',
      options: {
        space: 'YOUR_SPACE', // required
        accessToken: 'YOUR_ACCESS_TOKEN', // required
        host: 'cdn.contentful.com',
        environment: 'master',
        typeName: 'Contentful'
      }
    }
  ]
}
  • spaceやaccessTokenは先程、ContentfulのAPIKeysで取得したものです。
  • 直に書くのは厳しいので、Cloud上で実行する際、環境変数に登録することも考慮し、dotenvを使います。
$ yarn add dotenv
  • 一応環境ごとに(developmentのみでいいけど)作成しときます
  • .env.development

    CONTENTFUL_SPACE_ID=xxxxxxxxxxxx
    CONTENTFUL_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxx
    CONTENTFUL_HOST=cdn.contentful.com
  • gridsome.config.jsを書き換え
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
})

module.exports = {
  siteName: 'サイト名',
  plugins: [
    {
      use: '@gridsome/source-contentful',
      options: {
        space: process.env.CONTENTFUL_SPACE_ID, // required
        accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, // required
        host: process.env.CONTENTFUL_HOST,
        environment: 'master',
        typeName: 'Contentful'
      }
    }
  ]
}
  • これで一回実行します。

    • エラーが出た場合は、ContentfulのトークンやSpaceIDをサイド確認してみてください。
$ gridsome develop
もしくは
$ yarn run develop

先程のHello worldが表示されると思います。

表示するのにCSS frameworkを入れる

  • 今回はBulumaのVue版、buefyを使ってます。他のCSS Frameworkで全然OKです。
  • インストール
$ yarn add buefy
  • プロジェクトに読み込む
  • src/main.js
import DefaultLayout from '~/layouts/Default.vue'

import Buefy from 'buefy' // <- 追加
import 'buefy/dist/buefy.css' // <- 追加

export default function (Vue, { router, head, isClient }) {
  Vue.use(Buefy) // <- 追加

  // Set default layout as a global component
  Vue.component('Layout', DefaultLayout)
}

実際データを取得し、一覧にしてみる

  • src/pages/index.vue
<template>
  <Layout>
    <!-- Learn how to use images here: https://gridsome.org/docs/images -->
    <div class="columns items">
      <div class="column is-4" v-for="item in $page.allContentfulBlogPost.edges" :key="item.node.id">
        <g-link :to="'product/'+item.node.slug">
          <div class="card">
            <div class="card-image">
              <figure class="image is-4by3">
                <img :src="'/' + item.node.heroImage.file.url" alt="Placeholder image" />
              </figure>
            </div>
            <div class="card-content">

              <p class="title">
                {{ item.node.title }}
              </p>
              <div class="content">
                {{ item.node.description }}
                <br>
                <time datetime="2016-1-1">{{ item.node.publishDate }}</time>
              </div>
            </div>
          </div>
        </g-link>
      </div>
    </div>
  </Layout>
</template>

<script>
export default {
}
</script>

<style>
.items {
  margin-top: 50px;
  margin-bottom: 50px;
}
</style>

<page-query>
query Index {
  allContentfulBlogPost { // Model名が、Productだとすると、allContentfullProduct となる
    totalCount
    edges {
      node {
        // ここの下の内容はContentfulのモデル(BlogPost)に登録されているフィールド名のうち必要なもののみを取得
        id,
        title,
        slug,
        heroImage{
          file{
            url
          }
        }
        description,
        author{
          name,
          image{
            file{
              url
            }
          }
        }
        publishDate,
        tags,
        body,
      }
    }
  }
}
</page-query>
  • GraphQLについて、Contentful側のDocumentを参考にしてください。Imageはurlで取るしかなさそうです
  • ▶ 参考 Contentful Image API

これで記事の一覧ページができました。

記事詳細ページを作成する

  • 静的に作成されるべきですので、Build時にすべてのページを作成するようにします。
  • gridsome.server.js
  • 参考 ▶ (Create pages from GraphQL)[https://gridsome.org/docs/pages-api/#create-pages-from-graphql]
module.exports = function (api) {
  api.loadSource(({ addCollection }) => {
    // Use the Data Store API here: https://gridsome.org/docs/data-store-api/
  })

  api.createPages(async ({ graphql, createPage }) => {
    const { data } = await graphql(`{
      allContentfulBlogPost { // Model名が、Productだとすると、allContentfullProduct となる
        totalCount
        edges {
          node {
            // ここの下の内容はContentfulのモデル(BlogPost)に登録されているフィールド名を指定
            id,
            title,
            slug,
            heroImage{
              file{
                url
              }
            }
            description,
            author{
              name,
              image{
                file{
                  url
                }
              }
            }
            publishDate,
            tags,
            body,
          }
        }
      }
    }`)
    data.allContentfulBlogPost.edges.forEach(({ node }) => {
      createPage({
        path: `/product/${node.slug}/`, // 好きなpathに変更してください。
        component: './src/templates/BlogPost.vue', // 今から用意します。
        context: {
          node: node
        }
      })
    })
  })
}
  • 記事詳細の元となるVueのファイルを作成
  • src/templates/BlogPost.vue
<template>
  <Layout>
    <h1 class="title">{{ $context.node.title }}</h1>
    <dev v-html="$context.node.body"/>
  </Layout>
</template>
  • 実行する

    $ gridsome develop
  • すると、一覧ページの各記事のカードをクリックすると、詳細ページへ飛べるはずです。
  • またContentful側でContentsの内容を書き換え、再度ビルドすると、内容が書き換わってるのが、確認できます。

詳細ページを整える

  • Contentfulから取得する記事の内容(Body)はRichTextで記入されたMarkdown形式のデータが飛んできますので、MarkdowmをHTMLに変換します

  • 更に、Markdonw中にImageが含まれていたら(画像のHostingはContentful側)Lazyでロードしたいです

  • と、SEO上SiteのタイトルやdescriptionをContentfulでセットした内容を反映したい

これらの内容を反映していきます

Markdonwとimageのlazyロード

  • インストール
$ yarm add vue-lazyload markdown-it-vue markdown-it-image-lazy-loading
  • src/main.js

    ~
    import VueLazyload from 'vue-lazyload'
    ~
    ~
    export default function (Vue, { router, head, isClient }) {
    ~
    Vue.use(VueLazyload)
  • src/pages/index.Vueの画像をLazy対応する
<img :src="'/' + item.node.heroImage.file.url" alt="Placeholder image" /><img v-lazy="'/' + item.node.heroImage.file.url" alt="Placeholder image" />
  • src/templates/BlogPost.Vue
<template>
  <Layout>
    <h1 class="title">{{ $context.node.title }}</h1>
    <markdown-it-vue ref="markdownIt" class="md-body" :content="$context.node.body" :options="options" />
  </Layout>
</template>

<script>
// MarkdownItVueをロード
import MarkdownItVue from 'markdown-it-vue'
import 'markdown-it-vue/dist/markdown-it-vue.css'

// MarkdownItVueに含まれていない、image-lazy-loadingをロード
import MarkDownItLazyImage from 'markdown-it-image-lazy-loading'

export default {
  components: {
    MarkdownItVue
  },
  mounted() {
    // image-lazy-loadingをセット
    this.$refs.markdownIt.use(MarkDownItLazyImage)
  },
  data() {
    return {
      // markdown-it-vueのオプジョン設定
      options: {
        markdownIt: {
          linkify: true
        },
        linkAttributes: {
          attrs: {
            target: '_blank',
            rel: 'noopener'
          }
        },
        katex: {
          throwOnError: false,
          errorColor: '#cc0000'
        },
        icons: 'font-awesome',
        githubToc: {
          tocFirstLevel: 2,
          tocLastLevel: 3,
          tocClassName: 'toc',
          anchorLinkSymbol: '',
          anchorLinkSpace: false,
          anchorClassName: 'anchor',
          anchorLinkSymbolClassName: 'octicon octicon-link'
        }
      }
    }
  }
}
</script>

<style>
</style>

siteのmetaデータのセット

  • src/templates/BlogPost.VueをIndexに習って、metadataをセットします

metadata { title: this.$context.node.title, meta: [ { key: ‘description’, name: ‘description’, content: this.$context.node.description } ] }

  • gridsome.server.jsで、ページ作成時、contextでnodeを渡しているので、問題なさそうですが、しかし、なんと、$contenxtはしらんというエラーが出ました。
  • リサーチするとGithubのIssuesに行き着きました
metaInfo() {
  return {
    title: this.$context.title
  }
}
  • こう書いてましたんで、真似した所、$contextを取得できました。原因はわかりませんが。
~
export default {
  metaInfo() {
    return {
      title: this.$context.node.title,
      meta: [
        { key: 'description', name: 'description', content: this.$context.node.description }
      ]
    }
  },
~
~
  • これで、titleタグやmetaタグを自由に設定できそうですね。

まとめ

これで、一応一覧ページと詳細ページが出来上がりましたね。 Gridsomeはまだまだ微妙なところもありそうですが、いい感じに割と簡単にBlogサイトをHeadlessCMSと組み合わせて作ることができそうです。

ただ、Gatsbyと違って、image周りが軟弱でした。(g-imageというComponentがあったけどいまいち) なので、自前で他のものを使い、Lazyを実装したりしました。

とりあえずできたので良かったです。

今度は、これらをGridsome上でbuildし、S3か何かにDeployする方法を試したいと思います。

更に、ここにAmplify等入れて、認証も入れたいですね。

うかい / 株式会社UKAI
うかい@代表取締役兼エンジニア株式会社UKAI
Nobuyuki Ukai

株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。詳細はこちらから

🙏よかったらシェアお願いします🙏
WRITTEN BY
うかい / 株式会社UKAI
うかい@代表取締役兼エンジニア株式会社UKAI

Nobuyuki Ukai

株式会社UKAI 代表取締役CEO。建築を専攻していたが、新卒でYahoo!Japanにエンジニアとして入社。その後、転職、脱サラ、フリーランスエンジニアを経て、田舎へ移住し、ゲストハウスの開業、法人成り。ゲストハウス2軒、焼肉店、カフェ、食品製造業を運営しています。詳細はこちらから

CONACT
入力して下さい
Slack からもどうぞ

※お気軽にどうぞ!(6月20日まで有効!お早めに)

COPYRIGHT © 2020 UKAI CO., LTD. ALL RIGHTS RESERVED.