microCMS Nuxt.js(Composition API) Vuetifyの三銃士で30分でブログを構築する

# はじめに

microCMSはヘッドレスCMSサービスです。

管理画面から入稿して、APIを叩くと出力できるというものでセットアップが非常に簡単です。

簡単すぎるので30分でブログを作れます。

ということで作ります。

:::note warn
この記事ではVuetifyやComposition APIなどの細かい説明は省きます
:::

# microCMSの準備

microCMSのアカウント登録をして、管理画面に入るとテンプレートを選択しAPIを自動で作成できます。

![スクリーンショット 2022-06-23 10.51.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924363/64be2434-d20a-9541-0a95-4bbccd5c7b1b.png)

今回は「ブログ」を選択します。
すると勝手にAPIスキーマやらなんやら作成してくれます。すごい。

# Nuxtプロジェクトの準備

それでは、Nuxtの準備をしていきましょう。

yarnを利用していきます。

```terminal
$ yarn create nuxt-app blog
```

以下のように設定しました。
UI frameworkには、マテリアルデザインを簡単に扱えるVuetifyを導入しています。

```terminal
create-nuxt-app v4.0.0
✨  Generating Nuxt.js project in blog
? Project name: blog
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Vuetify.js
? Nuxt.js modules: Axios - Promise based HTTP client
? Linting tools: ESLint
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json
? Continuous integration: None
? Version control system: Git
```

## Nuxt Composition APIの導入

Vue 3のComposition APIを使えるように**Nuxt Composition API**を導入します。

https://composition-api.nuxtjs.org/

```terminal
$ yarn add @nuxtjs/composition-api
```

`nuxt.config.js`でモジュールを有効にします。

```js:nuxt.config.js
{
  buildModules: [
    '@nuxtjs/composition-api/module'
  ]
}
```

## nuxt-microcms-moduleの導入

microCMSとの連携をらくらくにしてくれるモジュールが公式から提供されていますので、これを導入します。

https://github.com/microcmsio/nuxt-microcms-module

```terminal
$ yarn add nuxt-microcms-module
```

```js:nuxt.config.js
{
  buildModules: [
    'nuxt-microcms-module'
  ]
},

microcms: {
  options: {
    serviceDomain: process.env.SERVICE_ID,
    apiKey: process.env.API_KEY
  },
  mode: process.env.NODE_ENV === 'production' ? 'server' : 'all'
},
```

サービスID(SERVICE_ID)とAPIキー(API_KEY)はmicroCMSの管理画面から確認できますので、それらを`.env`ファイルに記載しましょう。

## 日付フォーマットモジュールの導入

記事系のページだと日付を扱うことはほぼ必至ですので、日付を扱いやすくするモジュールを導入します。

```terminal
$ yarn add --dev @nuxtjs/date-fns
```

```js:nuxt.config.js
{
  buildModules: [
    '@nuxtjs/date-fns'
  ]
}
```

## sass-loaderの導入

```terminal
$ yarn add --dev sass sass-loader@10
```

## ESLintの設定

何かと`vue/multi-word-component-names`のエラーに引っかかるのでいくつか例外指定しておきます。

あとお好みですが、セミコロンの省略は個人的に気になるのでこちらも設定します。

```js:.eslintrc.js
module.exports = {
  //...
  rules: {
    'vue/multi-word-component-names': ['error', {
      ignores: ['default', 'index']
    }],
    semi: [
      'error',
      'always'
    ]
  }
};
```

# コーディング

## 共通部分の調整

Vuetifyを導入した状態でNuxtプロジェクトを準備すると、`layouts/default.vue`に既にいろいろ要素がありますので、これを整理します。

Vuetifyの公式ドキュメントには親切なことに[ワイヤフレーム](https://vuetifyjs.com/ja/getting-started/wireframes/#section-30b530f330d730eb)が用意されていますので、この中の`Base`を拝借します。
デフォルトで生成されているものを元に、Composition APIに置き換えつつ、ルーティングにもとりあえず`/blog`を書いておきましょう。

```vuejs:layouts/default.vue
<template>
  <v-app>
    <v-navigation-drawer
      v-model="drawer"
      :clipped="clipped"
      fixed
      app
    >
      <v-list>
        <v-list-item
          v-for="(item, i) in items"
          :key="i"
          :to="item.to"
          router
        >
          <v-list-item-action>
            <v-icon>{{ item.icon }}</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title v-text="item.title" />
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>
    <v-app-bar
      app
      :clipped-left="clipped"
      fixed
    >
      <v-app-bar-nav-icon @click="drawer = !drawer" />
      <v-toolbar-title>Blog</v-toolbar-title>
    </v-app-bar>
    <v-main>
      <v-container>
        <Nuxt />
      </v-container>
    </v-main>
    <v-footer :absolute="!fixed" app>
      <span>&copy; {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>

<script>
import { defineComponent } from '@vue/composition-api';

export default defineComponent({
  setup () {
    const clipped = false;
    const drawer = false;
    const fixed = false;
    const title = 'My Blog';
    const items = [
      {
        icon: 'mdi-home',
        title: 'Welcome',
        to: '/'
      },
      {
        icon: 'mdi-post',
        title: 'Blog',
        to: '/blog'
      }
    ];

    return {
      clipped,
      drawer,
      fixed,
      title,
      items
    };
  }
});
</script>
```

## index.vue

いまのところ書くこともないので世界に挨拶しておきます。

```vuejs:pages/index.vue
<template>
  <v-row justify="center" align="center">
    <v-col cols="2">
      Hello World!
    </v-col>
  </v-row>
</template>

<script>
import { defineComponent } from '@vue/composition-api';

export default defineComponent({
  name: 'IndexPage'
});
</script>
```

![index.vue](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924363/28ac6a16-37af-f702-cc3d-8ff04b3c62d3.png)


## 記事一覧ページ

ブログ記事の一覧ページを作成します。

`pages/blog/index.vue`を作成します。

```vuejs:pages/blog/index.vue
<template>
  <v-row>
    <v-col
      v-for="blog in blogs.contents"
      :key="blog.id"
      cols="12"
      md="4"
      fill-height
    >
      <v-card height="100%" nuxt :to="`/blog/${blog.id}/`">
        <v-img
          contain
          :src="blog.eyecatch.url"
          :alt="blog.title"
          aspect-ratio="1.7778"
          class="mx-auto"
        />
        <v-card-title>
          {{ blog.title }}
        </v-card-title>
      </v-card>
    </v-col>
  </v-row>
</template>

<script>
import {
  defineComponent,
  ref,
  useMeta,
  useFetch,
  useContext
} from '@nuxtjs/composition-api';

export default defineComponent({
  name: 'BlogIndex',
  setup () {
    useMeta({
      title: 'Blog',
      meta: [
        {
          hid: 'description',
          property: 'description',
          content: 'ブログです。'
        }
      ]
    });

    const blogs = ref({});
    const { $microcms } = useContext();

    useFetch(async () => {
      blogs.value = await $microcms.get({
        endpoint: 'blogs'
      });
    });

    return { blogs };
  },
  head: {}
});
</script>
```

![ブログ一覧](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924363/c32c02b6-152d-4eba-d878-013052407981.png)


## 記事詳細ページ

Nuxtだと`_id`という感じでアンダースコアのプレフィックスをつけることで動的ルーティングが可能です。

```vuejs:pages/blog/_id/index.vue
<template>
  <v-row justify="center" align="center">
    <v-col cols="10">
      <span>
        <time
          :datetime="blog.publishedAt"
          v-text="formattedDate"
        >
          {{ blog.publishedAt }}
        </time>
      </span>
      <section>
        <div class="bl_wysiwyg" v-html="blog.content" />
      </section>
    </v-col>
  </v-row>
</template>

<script>
import { defineComponent, ref, useMeta, useContext, useFetch } from '@nuxtjs/composition-api';

export default defineComponent({
  setup () {
    const blog = ref({});
    const formattedDate = ref('');
    const { $microcms, params, $dateFns } = useContext();

    const formatDate = (date) => {
      return $dateFns.format(new Date(date), 'yyyy年MM月dd日');
    };

    useFetch(async () => {
      blog.value = await $microcms.get({
        endpoint: `blogs/${params.value.id}`
      });
      formattedDate.value = formatDate(blog.value.publishedAt);
    });

    useMeta(() => ({
      title: `${blog.value.title}`,
      meta: [
        {
          hid: 'description',
          property: 'description',
          content: blog.value.title
        }
      ]
    }));

    return { blog, formattedDate };
  },
  head: {}
});
</script>
```

なんとなくデザインが味気ないので、Vuetifyのテキストコンポーネントを元にスタイルをつけておきます。

```scss
<style lang="scss" scoped>
.bl_wysiwyg {
  margin-top: 30px;

  &::v-deep {
    h1,
    h2,
    h3,
    h4 {
      margin-top: 2.0em;
      margin-bottom: 2.4rem;
      padding-bottom: 0.1rem;
      border-bottom: solid 1px #ddd;
    }
    h1 {
      font-size: 3rem;
      letter-spacing: normal;
      font-weight: 400;
    }
    h2 {
      border-bottom: solid 1px #ddd;
      font-size: 2.125rem;
      letter-spacing: 0.0073529412em;
      font-weight: 400;
    }
    h3 {
      font-size: 1.5rem;
      letter-spacing: normal;
      font-weight: 400;
    }
    h4 {
      font-size: 1.25rem;
      letter-spacing: 0.0125em;
      font-weight: 500;
    }
    p {
      margin-top: 1rem;
      font-size: 1.08rem;
      letter-spacing: 0.03125em;
      font-weight: 400;
      line-height: 1.8;
      img {
        margin: 1.25rem auto;
        width: 100%;
        height: auto;
      }
    }
  }
}
</style>
```

![ブログ詳細](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924363/6ee0fe00-3196-6aa3-763d-5a268527f5a1.png)


# できあがり

なんともう出来てしまいました。

あとは、ブログのカテゴリごとのページや記事プレビュー画面なども簡単にできちゃうので、興味が湧いた方は是非そちらも挑戦してみてください。