🦢

Nuxt.jsとmicroCMSでJamstack入門

2021/12/09に公開

この記事は microCMS Advent Calendar 2021 10日目の記事です。

初めてAdvent Calendarに参加させて頂きます。現在WEBのフロントエンド周りの技術を中心に学習中で、その学習ノートのような形で記事を書いています。内容に誤り等ありましたらコメント等で教えていただけると幸いです。よろしくお願いします。

はじめに

Nuxt.jsとmicroCMSを使用して、Jamstack構成の個人ブログを作成しました。
https://chabatake-web.com/

主な技術構成は以下の通りです。

  • Nuxt.js(v2.15.7)(SSG)
  • microCMS(ヘッドレスCMS)
  • Tailwind CSS(v2.2.16)(CSSフレームワーク)
  • Netlify(ホスティング)

以前に PWA Night Conference 2021 というオンラインのイベントに参加した際に、microCMSさんのワークショップで紹介されていた内容を元に作成したものです。
microCMSの導入とNuxtプロジェクトの作成~Netlityへのホスティングまでの一連の流れが、この通りにやるとほぼ完成してしまいます...凄。以下のmicroCMSブログでも同内容の詳細が解説されています。
https://blog.microcms.io/microcms-nuxt-jamstack-blog

今回はこのチュートリアルを元に作成したものに、いくつか機能を追加してみたことについて書いてみようと思います。以下のような内容です。

  • Tailwind CSSの導入
  • 目次の作成
  • SNSのシェアボタン作成

Tailwind CSSの導入

いきなり機能追加という趣旨から少しずれますが、Tailwind CSSの導入についてです。Tailwind CSSはユーティリティファーストのCSSフレームワークです。
https://tailwindcss.com/
Vue.jsやReact等のライブラリ/フレームワークを使用して、コンポーネント指向でWEBサイト/アプリケーションを構築する際に相性がよいなと個人的に感じていて使用しています(CSS設計においては様々な選択肢、考え方があると思われますが語れる力がないので今回は割愛します...)。
2021.12.8現在のバーションは、Pre-releaseとしてv3.0.0-alpha.2が出ています。今回はv2.2.16を使用しています。

Nuxt.jsプロジェクトに以下の手順でTailwind CSSを導入します。
https://v2.tailwindcss.com/docs/guides/nuxtjs

最初にnpmから以下のパッケージをインストールします。

$ npm install -D @nuxtjs/tailwindcss tailwindcss@latest postcss@latest autoprefixer@latest

インストールした@nuxtjs/tailwindcssをNuxtの設定ファイルに記述します。

./nuxt.config.js
export default {
  buildModules: ["@nuxtjs/tailwindcss"],
};

次に、以下のコマンドでtailwindの設定ファイルを生成します。

$ npx tailwindcss init

生成されたtailwind.config.jspurgeオプションの記述を書き換えます。purge対象のファイルを記述することで、ビルド時に必要なスタイルのみが書き出されます(この設定を行わないとtailwindの全てのスタイルを持った巨大なcssファイルが書き出されてしまいます→Optimizing for Production)。
また、こちらは必須ではありませんがmodeオプションをjitとすることで、v2.1から導入されたJust-in-Time Modeが利用できます。こちらを利用するとビルド時間の高速化や、オプション扱いのバリアントがデフォルトで利用できたり、任意のサイズ・カラー等が使用できたりします(v3からは標準機能となる予定です)。→Just-in-Time Mode

./tailwind.config.js
 module.exports = {
+  mode: 'jit',
-  purge: [],
+  purge: [
+    './components/**/*.{vue,js}',
+    './layouts/**/*.vue',
+    './pages/**/*.vue',
+    './plugins/**/*.{js,ts}',
+    './nuxt.config.{js,ts}',
+  ],
   darkMode: false, // or 'media' or 'class'
   theme: {
     extend: {},
   },
   variants: {
     extend: {},
   },
   plugins: [],
 };

最後に./assets/css/内にcssファイルを作成し、以下の記述を追加します。

./assets/css/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;

以上でプロジェクト内でTailwind CSSが利用できるようになりました。

例として、ブログ内のフッターコンポーネントのテンプレート部分は以下のような記述としています。

LayoutFooter.vue
<template>
  <footer
    class="
      col-span-2
      py-[16px]
      border-t-[1px] border-warmGray-400
      dark:border-warmGray-600
    "
  >
    <p
      class="
        text-center text-[14px] text-warmGray-800
        leading-relaxed
        whitespace-nowrap
        dark:text-warmGray-200
      "
    >
      Copyright © 2021 koyama shigehito
    </p>
  </footer>
</template>

ユーティリティクラスをガシガシと書いていくような形で最初は少し抵抗がありましたが、慣れてくると離れられなくなりそうで怖いです...。

目次の作成

次に、記事内の見出しを元に目次を作成しました。スクリーンサイズが大きい時のみ(PCサイズ想定)サイドエリアに以下のような形で表示させています。

こちらも以下のmicroCMSブログを参考にさせて頂きながら作成しています。

https://blog.microcms.io/create-table-of-contents/

記事の本文はmicroCMSのリッチエディタを利用しているので、HTMLの構文解析を行って記事内の見出しタグを抜き出して表示しています。

まずcheerioというHTMLパース用のライブラリをインストールします。

$ npm install cheerio

cheerioを使用して各記事内の見出し部分を抜き出します。抜き出した見出しの配列をmaptext id nameの配列に整形して返却しています。
(今回はh1h2タグを抜き出していますが、タイトル等でh1タグを使用している場合、見出しにh1タグは使用しなほうが良かったかもしれません...)

./pages/_slug/index.vue
<script>
export default {
  // ...略
  async asyncData({ $microcms, params }) {
    // nuxt-microcms-moduleを利用してデータを取得
    const data = await $microcms.get({
      endpoint: `blog/${params.slug}`,
    });
    // 目次用に見出しの抜き出し(h1、h2タグ内容)
    const $ = cheerio.load(data.body);
    const headings = $("h1, h2").toArray();
    // 取得した配列を整形
    const tableOfContent = headings.map((data) => ({
      text: data.children[0].data,
      id: data.attribs.id,
      name: data.name,
    }));
    return {
      ...data,
      tableOfContent,
      // ...略
    };
  },
  // ...略
</script>

そして、返却した配列を以下のような目次コンポーネント内のpropsで受け取ってリスト表示させています。h1h2の各クラスは関数で動的に付与しています。(template内のスタイルの記述は省略しています)。

LayoutTbleOfContent.vue
<template>
  <div>
    <h2>目次</h2>
    <!-- 目次の内容をリスト表示 -->
    <ul>
      <li
        v-for="item in tableOfContent"
        :key="item.id"
        :class="tableOfContentClass(item.name)"
      >
        <n-link v-scroll-to="`#${item.id}`" to>
          {{ item.text }}
        </n-link>
      </li>
    </ul>
  </div>
</template>

<script lnag="ts">
import { defineComponent } from "@nuxtjs/composition-api";

export default defineComponent({
  name: "LayoutTableOfContent",
  // propsで目次データの受け取り
  props: {
    tableOfContent: {
      type: [Object, Array],
      required: true,
    },
  },
  setup() {
    // h1、h2のクラス用の関数
    const tableOfContentClass = (paragraph) => {
      if (paragraph === "h1") {
        return " // ここにh1のクラスを記述する // ";
      } else if (paragraph === "h2") {
        return " // ここにh2のクラスを記述する // ";
    };
    return {
      tableOfContentClass,
    };
  },
});
</script>

また、リンク箇所へのスムーススクロールにvue-scroll-toというライブラリを使用しています。
こちらもインストールします。

$ npm install vue-scrollto

pluginsディレクトリ内に以下のようなjsファイルを作成します。durationeasing等は好きな値に調整します。

./plugins/vue-scrollto.js
import Vue from 'vue';
import VueScrollTo from 'vue-scrollto';

Vue.use(VueScrollTo, {
  // 各プロパティの値を調整していい感じにする
  duration: 500,
  easing: [0, 0, 0.1, 1],
  offset: -100,
});

そしてnuxt.config.jsのpluginsオプションにvue-scrolltoを登録します。

./nuxt.config.js
export default {
  plugins: [
    '~plugins/vue-scrollto'
  ]
}

これでvue-scrolltoが利用できるようになりました。microCMSでは各見出しタグにidが振られているので、以下のように目次から各見出しに対してアンカーリンクを張る形で実装できました。

LayoutTbleOfContent.vue(一部抜粋)
<n-link v-scroll-to="`#${item.id}`" to>
  {{ item.text }}
</n-link>

SNSのシェアボタン作成

最後にSNSのシェアボタン作成についてです。今回は、以下のようにtwitterのみのシンプルな形で実装しています。

親コンポーネントからpropsでタイトルを取得して表示するようにしています。また、ボタン画像については、以下のVue.jsのドキュメントを参考にBaseIconというコンポーネントを作成してSVG画像で管理しています。
https://jp.vuejs.org/v2/cookbook/editable-svg-icons.html
実装は以下のようになりました(template内のスタイルの記述は省略しています)。

BaseSnsShareButton.vue
<template>
  <div>
    <span>share</span>
    <a :href="twitterURL" target="_blank" rel="noopener">
      <BaseIcon width="16" height="16" :icon-name="'twitter'">
        <IconTwitter />
      </BaseIcon>
    </a>
  </div>
</template>

<script lnag="ts">
import { computed, defineComponent, useContext } from '@nuxtjs/composition-api';
// アイコン画像用のコンポーネントの読み込み
import BaseIcon from './BaseIcon.vue';
import IconTwitter from './icons/IconTwitter.vue';

export default defineComponent({
  name: 'BaseSnsShareButton',
  components: { BaseIcon, IconTwitter },
  props: {
    text: {
      type: String,
      default: '',
    },
  },
  setup(props) {
    const { route } = useContext();
    // 各記事のurlを取得
    const path = route.value.path;
    const url = `https://chabatake-web.com${path}`;
    // 各記事のタイトルを取得
    const textAndHashTag = encodeURIComponent(`${props.text}`);
    const twitterURL = computed(
      () => `https://twitter.com/intent/tweet?url=${url}&text=${textAndHashTag}`
    );
    return {
      twitterURL,
    };
  },
});
</script>

OGP画像の設定については以下の記事を参考にさせて頂きました。
https://mykii.blog/nuxt-netlify-web-release-settings/

参考記事と同様にnuxt.config.jsに共通部分を記述して各ページ(pages_slug/index.vue)から上書きするようにしています。

nuxt.config.js
export default {
  head: {
    title: "chabatake WEB",
    htmlAttrs: {
      lang: "ja",
      prefix: "og: http://ogp.me/ns#",
    },
    meta: [
      // ...略
      {
        hid: "og:site_name",
        property: "og:site_name",
        content: "chabatake WEB",
      },
      { hid: "og:type", property: "og:type", content: "website" },
      {
        hid: "og:url",
        property: "og:url",
        content: "https://www.chabatake-web.com",
      },
      { hid: "og:title", property: "og:title", content: "chabatake WEB" },
      {
        hid: "og:description",
        property: "og:description",
        content:
          "主にWEBのフロントエンド周りの技術やデザインについての学習内容、個人的な関心事についてまとめているブログです。",
      },
      {
        hid: "og:image",
        property: "og:image",
        content: "https://www.chabatake-web.com/images/chabatake-web_OGP.png",
      },
      { name: "twitter:card", content: "summary" },
    ],
    // ...略
  },
  // ...略
};
pages_slug/index.vue
<script>
export default {
  // ...略
  head() {
    return {
      title: this.title,
      meta: [
        {
          hid: "description",
          name: "description",
          content: this.description,
        },
        { hid: "og:title", property: "og:title", content: this.title },
        {
          hid: "og:description",
          property: "og:description",
          content: this.description,
        },
        {
          hid: "og:url",
          property: "og:url",
          content: `https://www.chabatake-web.com/${this.id}/`,
        },
        {
          hid: "og:image",
          property: "og:image",
          content: "https://www.chabatake-web.com/images/chabatake-web_OGP.png",
        },
        { name: "twitter:card", content: "summary" },
        { hid: "og:type", property: "og:type", content: "article" },
      ],
    };
  },
};
</script>

アイコンをクリックすると、twitterで以下のようにカード表示されるようになりました!

おわりに

以上、Nuxt.jsとmicroCMSで作成したブログについて紹介させて頂きました。WEBサイトを作成して公開すること自体が今回初めてだったので、色々と拙い部分がありますが、せっかく作ったものなので勉強しながら修正していけたらいいなと思っています。

今後やりたいこととして、現状Nuxt2を使用していますが、Nuxt3のベータ版がリリースされているので、正式リリースを待ってそちらに移行していけたら良いなと思っています。(以下のmicroCMSブログで早速Nuxt3を利用した解説がありました!)
https://blog.microcms.io/nuxt3-create-blog/

おまけ

カレンダー1日目のりゅーそうさんの記事でmicroCMSの「推し」機能を募集されていたので、僭越ながら発表させて頂きます。

私の推し機能は...

nuxt-microcms-moduleです!
(機能とかではないかもしれません!)
このmoduleなしにサイトは完成しませんでした。ありがとうございます!

参考

GitHubで編集を提案

Discussion