🙆

[デプロイまでわずか数時間!]Vue.jsとmicroCMSでブログ作成して、Vercelで公開するまでの楽ちんロードマップ

2023/12/12に公開

0. はじめに

0-1. この記事は何?

LAPRAS のアドベントカレンダーの8日目のエントリーです!(大遅刻です...すみません!)

この記事の通りにやれば、定時後にサクッとVue.jsとmicroCMSを用いたオリジナルブログを作成し、それをVercelでデプロイするところまでやれちゃいます。

じゃあ、なんでそんなサクッと終わる事なのにアドベントカレンダー遅刻したんですか?

対象読者さんとしては

  • GitHubのアカウントを持っていて普通に使っている
  • JS/TS及びVue.jsを触ったことはある

というレベルでOKです。

ヘッドレスCMSってなに?VercelとかNetlifyみたいなWebホスティングのサービス触ったこと無いよ...という方でも大丈夫です!

また今回の記事のレベルの使用ならば、microCMSもVercelも無料で使えちゃいます!

0-2. 使用する技術と用語

フロントエンド:Vue.jsNES.css

フロントエンドにはVue.jsの3系を使います。

また何となく今回作るブログは8bitライクなファミコン風のデザインにしたいなと思ったので、Nes.cssというCSSフレームワークを使うことにしました。

後ほどNes.cssとそれに合った日本語フォントの導入について解説していきます。

ヘッドレスCMS:microCMS

そもそもヘッドレスCMSとは?という話ですが読んで字のごとく「フロントエンド(ヘッド)がないコンテンツ管理システム(Contents Management System)」の事です。

逆に(ヘッドがある)通常のCMSだと、有名なのはWordPressとかが有名ですね。

ヘッドレスCMSでは、APIのみ提供されるので、逆にコンテンツ表示側の技術に好きなものを使える...という感じですね。(それ以外にも色々と利点はあるようなのですが、ここでは割愛。)

microCMSはそんなヘッドレスCMSの1つです。

日本製のため、日本語ドキュメントも充実していますし、API管理の画面もスッキリしていて使いやすいです。

デプロイホスティングサービス:Vercel

VercelはCI/CDとWebサーバーが合わさった様なサービスで、GitHubなどのリポジトリと連携することで手軽にアプリケーションをデプロイすることが可能にします。

Next.jsと相性が良い(らしい)ですが、Vue.jsにも対応してくれています。

比較対象としてよく挙がる他のホスティングサービスだと、NetlifyGitHub Pages辺りかなと思います。

0-3. 下準備

ここではVue.jsのプロジェクトを用意して必要なライブラリをnpmインストールするところまで行います。

まずはVue.jsのプロジェクトのセットアップから。
今回、自分はVue.jsのバージョン3.8を使用しています。
npm init vue@{{指定バージョン}}}とすると、セットアップの為の質問がされるので下記に従って進めてください。

$ npm init vue@latest
Need to install the following packages:
create-vue@3.8.0
Ok to proceed? (y) y
✔ Project name: … {{ お好きなブログ名 }}
✔ Package name: … デフォルトでOK
✔ Add TypeScript? … Yes
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes
$ cd {{作成したブログのディレクトリ}}
$ npm install

続いて必要なライブラリを入れていきます。
nes.cssに関しては今回の自分のブログを8bit風のUIにするのに使っているだけなので好みに応じて入れる入れないを判断してください。

-- レトロなファミコンゲーム風のUIを作るのに用いるCSSフレームワーク
$ npm install nes.css
-- HTTP クライアント(後ほどmicroCMSの提供するAPIを叩くのに必要)
$ npm install axios
-- VueのUIフレームワーク(カード画面を作るのに便利だからインストールしておく)
$ npm install element-plus --save

最後に今回使用する日本語フォントのNu きなこもちをリンク先のboothからインストール・unzipで解凍しておいてください。

ここまで出来たら下記のような画面が立ち上がるか確認してみます。

$ npm run dev
  VITE v5.0.7  ready in 173 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

1. とりあえずローカルで動くそれっぽく動くブログをVue.jsで作成

最終的には下記のディレクトリのツリーのような状態になることを目指していきます。

$ tree
├── README.md
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── api
│   │   ├── articles
│   │   │   └── index.ts
│   │   └── home
│   │       └── index.ts
│   ├── assets
│   │   ├── fire-icon.png
│   │   ├── fonts
│   │   │   └── NuKinakoMochi-Reg.otf
│   │   └── main.css
│   ├── components
│   │   ├── BlogCard.vue
│   │   ├── TheWelcome.vue
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   └── views
│       ├── AboutView.vue
│       ├── ArticleView.vue
│       └── HomeView.vue

まだヘッドレスCMS(microCMS)を用いたapiの用意はしていないので仮のベタ書きハードコーディングでそれっぽくローカルで動くブログを作るところまでをここでは目指します。

1-1. インストールしたフォントを使えるように+アイコンを作成

src/assets下にfontsというディレクトリを作成し、先程ダウンロードしたNuKinakoMochi-Reg.otf を配置してください。

続いて、同じくsrc/assets下にあるmain.cssのデフォルトの中身を全て消した上で下記のように@font-faceを用いた独自フォントの設定を記述してください。

@font-face {
  font-family: "NuKinakoMochiRegFont";
  src: url("./fonts/NuKinakoMochi-Reg.otf") format("opentype");
}

これでNuKinakoMochiRegFontという名前でフォントを使えるようになります。

ついでにassetsにfire-icon.pngという名前の画像を入れておいてください。
ちなみに自分はchatGPT先生にアイコン画像生成をお願いしました。

1-2. vue-routerやnes.css、element-plusを使えるように

vue-routerに関してはnpm init vue@latestで入れている場合、デフォルトで使える状態のはずですが、それ以外のnes.cssやelement-plusに関しては下記のようにsrc/main.tsを設定しないといけません。

import './assets/main.css'
import ElementPlus from "element-plus";
import 'nes.css/css/nes.min.css';
import 'element-plus/dist/index.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)
app.use(ElementPlus)

app.mount('#app')

1-3. ホーム画面を実装

Vue.jsのプラグインの一つ、Vue Routerを利用することでSPA(シングルページアプリケーション)を実現しています。

まずはナビゲーターで切り替えする部分を作っていきます。

App.vueの実装
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <div class="site">
    <header>
      <RouterLink class="title-link" to="/" >
        <div class="title">
          <img src="@/assets/fire-icon.png" class="icon"/>
          <h1>くまもとブログ</h1>
        </div>
      </RouterLink>

      <div class="nav">
        <RouterLink to="/" >
          <button class="nes-btn">ブログ</button>
        </RouterLink>
        <RouterLink to="/about">
          <button class="nes-btn">プロフィール</button>
        </RouterLink>
      </div>
    </header>

    <div class="main-content">
      <RouterView />
    </div>

    <div class="footer">
      ふったーだよ
    </div>
  </div>
</template>

<style lang="scss" scoped>
.site{
  background-color: gray;
  color: white;
  font-family: "NuKinakoMochiRegFont";
  padding: 0 30%;
  min-height: 100vh;
  display: flex;
  flex-flow: column;
}
.main-content {
  flex: 1;
}
header {
  padding: 15px;
  display: flex;
  flex-wrap: nowrap;
  justify-content: space-between;
  border-bottom: black solid 1px;
}
.nes-btn {
  margin-top: 10px;
  margin-left: 20px;
}
.title-link{
  text-decoration: none;
  color: inherit;
}
.title{
  display: flex;
  font-size: 20px;
  margin-top: 8px;
  .icon {
    width: 50px;
    height: 50px;
    margin-top: 5px;
    margin-right: 10px;
  }
}
.footer{
  text-align: center;
}
</style>

ルーティング制御に関してはsrc/router/index.ts下で設定していきます。
デフォルトの書き方を参考に書いていきましょう。

ArticleViewに関しては後ほど実装していきます。

src/router/index.tsでルーティング制御の実装
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
import AboutView from '@/views/AboutView.vue'
import ArticleView from '@/views/ArticleView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      component: AboutView
    },
    {
      path: '/articles/:id',
      name: 'article',
      component: ArticleView
    }
  ]
})

export default router

続いてホーム画面の際に表示されるHomeViewと、その中で一覧表示されるカードのコンポーネントを実装していきます。

HomeViewに関してはsrc/viewsにあるデフォルトのHomeView.vueの中身を全て消してそのまま利用してしまいましょう。

HomeViewの実装
<script setup lang="ts">
import { ref } from 'vue'
import BlogCard from "@/components/BlogCard.vue";
type Blog = {
  articleId: string
  title: string
  imgUrl: string
  content: string
  publishedAt: string
}
const blogList = ref<Blog[]>()
// TODO: APIから取得したデータを入れるまでの仮のデータ
blogList.value = [
  {
    articleId: 'id3',
    title: 'エアロバイクを漕いだ後に会議に行くとバレるという話',
    imgUrl: 'https://sakidorico.s3.amazonaws.com/wp/wp-content/uploads/2023/09/64f1787f8af60.jpg',
    content: '<h1>エアロバイクを漕いだ後に会議に行くとバレるという話</h1>',
    publishedAt: '2023/12/09',
  },
  {
    articleId: 'id2',
    title: '俺が好きなサウナを紹介させてくれ!!',
    imgUrl: 'https://manager.reservation.jp//cmn/img/facility/10000001/M/1598601781785219.jpg',
    content: '<h1>俺が好きなサウナを紹介させてくれ!!</h1>',
    publishedAt: '2023/12/08',
  },
  {
    articleId: 'id1',
    title: 'このブログについて',
    content: '<h1>このブログについて</h1>',
    publishedAt: '2023/12/1',
  },
]
</script>

<template>
  <div class="card-container">
    <BlogCard
        v-for="blog in blogList"
        :key="blog.articleId"
        :blog="blog"
    />
  </div>
</template>
<style lang="scss" scoped>
.card-container {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
}
.content {
  background-color: pink;
}
.time {
  font-size: 12px;
  color: #999;
}

.bottom {
  margin-top: 13px;
  line-height: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.button {
  padding: 0;
  min-height: auto;
}

.image {
  width: 100%;
  display: block;
}
</style>

カードのコンポーネントに関してはsrc/components下に新規にBlogCard.vueファイルを作ってください。

カードコンポーネント(BlogCard)の実装
<script setup lang="ts">
defineProps({
  blog: {
    type: Object,
    required: true,
  },
})
</script>

<template>
  <div class="card">
    <router-link :to="{ name: 'article', params: { id: blog.articleId } }">
      <el-card :body-style="{ padding: '0px' }">
        <img
            class="image"
            :src="blog.imgUrl ?? 'https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png'"
        />
        <div style="padding: 14px">
          <span>{{ blog.title }}</span>
          <div class="bottom">
            <time class="time">{{ blog.publishedAt }}</time>
            <el-button text class="button">Open</el-button>
          </div>
        </div>
      </el-card>
    </router-link>
  </div>
</template>

<style lang="scss" scoped>
.card {
  width: 300px;
  margin: 30px auto;
  border: black solid 3px;
}
.content {
  background-color: pink;
}
.time {
  font-size: 12px;
  color: #999;
}

.bottom {
  margin-top: 13px;
  line-height: 12px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.button {
  padding: 0;
  min-height: auto;
}

.image {
  width: 100%;
  height: 250px;
  display: block;
}
</style>

後は、src/viewsディレクトリ下にArticleView.vueのファイルを作っておいてください。
一旦、下記のような仮の内容でおkです。

各記事のコンポーネント(ArticleView.vue)の実装
<script setup lang="ts">
</script>

<template>
<h1>テストだよ</h1>
</template>

<style lang="scss" scoped>
</style>

この時点でnpm run devをして下記のような見た目のブログが出来上がっていれば、OKです!
一旦、コーヒーブレイクしませう!

  • localhostの直下(Home画面)

  • 各記事のリンク先

1-4. 各記事の中身に当たる部分の実装とプロフィールページの実装

さて先程、テキトーに実装したArticleView.vueの中身を書いていきます。

と言っても今回は簡単に済ませるためにVue.jsにビルトイン(デフォルトの組み込み)で入っているディレクティブのv-htmlを用いるサボりお手軽実装です。

※当然ですが、ユーザーが書き込みを行えるエリアにv-htmlを用いるのはXSS(クロスサイトスクリプティング)の危険性があるので辞めましょう

下記のTODOコメントに書いているようにここも後ほど、apiを実際に叩くように変えていきます。

ArticleView.vueの中身の実装
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const isLoaded = ref<boolean>(false)
const articleId = ref<string>('')
// TODO: 後ほど型を定義する
const blog = ref<any>()

onMounted(async () => {
  if (Array.isArray(route.params.id)) {
    articleId.value = route.params.id.join(',')
  } else {
    articleId.value = route.params.id
  }
  // TODO: 後ほどAPIから取得するようにする
  blog.value = {
    title: 'エアロバイクを漕いだ後に会議に行くとバレるという話',
    ogpUrl: 'https://sakidorico.s3.amazonaws.com/wp/wp-content/uploads/2023/09/64f1787f8af60.jpg',
    content: '<h1>エアロバイクを漕いだ後に会議に行くとバレるという話</h1>',
    published: '2023/12/09',
    articleId: articleId.value
  }
  isLoaded.value = true
})

</script>

<template>
  <div v-if="isLoaded">
    <div class="blog-cotent" v-html="blog.content"></div>
  </div>
</template>

続いてプロフィールページを実装します。

ここも簡単のために、SNSのリンクを載せるだけのお手軽仕様にしておきます。
なおSNSリンクもnes.cssのおかげで8bit風のデザインのアイコンを簡単に導入できます。

ファイルはデフォルトであるsrc/views/AboutView.vueの中身を丸っと消して再利用します。

AboutView.vueの中身の実装
<template>
  <div class="profile">
    <p class="nes-balloon from-left nes-pointer is-dark">
      うぉおおおおおおおお!!フォローしてね!!!
    </p>
    <i class="nes-bcrikko"></i>
    <div class="sns">
      <a href="https://twitter.com/digitalhimiko">
        <i class="nes-icon twitter is-large"></i>
      </a>
      <a href="https://github.com/kumamoto-hamachi"><i class="nes-icon github is-large"></i></a>
    </div>
  </div>
</template>

<style>
.profile{
  margin-top: 100px;
}
.nes-balloon{
  display: block;
}
.sns{
  display: flex;
  justify-content: space-around;
  flex-wrap: nowrap;
  margin-top: 120px;
}
</style>

ここまで実装出来たら、先ほどと同様にnpm run devしてまた動作確認してやりましょう。

  • 各記事のリンク先

  • プロフィールページ

これでローカルで動くそれっぽいブログは作れました。お疲れ様でした。

2. microCMSとの繋ぎこみ

2-1. microCMSの登録からサービス登録、APIのセットアップまで

microCMSに登録されていない方はサインアップからお願いします。

登録後は「サービスを作成」で「一から作成」を選択。

下記画面にて「サービス名」と「サービスID」を決めてください。
※記載の通りこれは後から変更可能なのでテキトーでOK

サービス名を決められたら、次はAPIのセットアップです。

今回はブログを作っていくので素直に下記にてブログのテンプレートを選択します。

下記のようなAPI一覧画面が出てきたらOKです!かんたん!楽ちん!

2-2. APIの確認と作成

先程の一覧で「APIプレビュー」を押してみてください。

各言語やコマンドでどのようにapiを叩けば良いか、どんな結果が返ってくるかが確認出来ます。下記だと記事一覧が返ってくるAPIみたいですね。

それでは「APIの作成」や「コンテンツのコピー」で実際に1つ記事を書いていきましょう。

2-3. APIキーの取得と設定

ある程度記事も書けてきたら、次は先程作ったローカルのVue.js製ブログでapiを立たけるようにapiキーを設定します。

まずはmicroCMSのサービス設定でAPIキーを取得してください。
(外部にお漏らしをしないように、扱い注意してね!)

次にローカル環境のブログのディレクトリ直下の.env.localに下記のように環境変数を設定してあげてください。()

export VITE_HEADLESS_CMS_API_KEY=xxxxx

なお環境変数のprefix(頭文字)には必ずVITE_と付けるようにしてください。
※参考:Vite:Env Variables and Modes

2-4. APIを実際に叩いてみる

src直下に下記のような構成のディレクトリ及びファイルを作ってください。

├── src
│   ├── api
│   │   ├── articles
│   │   │   └── index.ts
│   │   └── home
│   │       └── index.ts

それぞれのindex.tsにaxiosでHTTPクライアントの実装をしていきます。

homeの画面用のapi(src/api/home/index.ts)
import axios from "axios";

class Get {
    public async execute()  {
        // 記事一覧を取得する
        const response = await axios.get(
            "https://kumamoto-blog.microcms.io/api/v1/blogs/",
            {
                headers: {"X-API-KEY": import.meta.env.VITE_HEADLESS_CMS_API_KEY}
            }
        );
        return response.data.contents.map((blog: any) => {
            return {
                articleId: blog.id,
                title: blog.title,
                imgUrl: blog.eyecatch.url,
                content: blog.content,
                publishedAt: blog.publishedAt,
            };
        });
    }
}
export default new Get();
articlesの画面用のapi(src/api/articles/index.ts)
import axios from "axios";

class Get {
    public async execute(articleId: string) {
        // 特定の記事1つを取得する
        const response = await axios.get(
            "https://kumamoto-blog.microcms.io/api/v1/blogs/" + articleId,
            {
                headers: {"X-API-KEY": import.meta.env.VITE_HEADLESS_CMS_API_KEY}
            }
        );
        const res =  response.data;
        return {
            articleId: res.id,
            title: res.title,
            imgUrl: res.eyecatch.url,
            content: res.content,
            publishedAt: res.publishedAt,
        }
    }
}
export default new Get();

上で実装したそれぞれのGetを、HomeView.vueArticleView.vueで使えるようにしていきます。

まずはHomeView.vueから。

先ほど書いたblogListのハードコーディング部分を消した上で、下記のように書き換えしてください。

<script setup lang="ts">
import  Get from "@/api/home/index"
onMounted(async () => {
  blogList.value = await Get.execute()
})

</script>

続いてArticleView.vue。こちらもapi/articles/indexをimportした上で、onMountedのハードコーディング部分を書き換えしてください。

<script setup lang="ts">
import Get from '@/api/articles/index'

onMounted(async () => {
  if (Array.isArray(route.params.id)) {
    articleId.value = route.params.id.join(',')
  } else {
    articleId.value = route.params.id
  }
  blog.value = await Get.execute(articleId.value)
  isLoaded.value = true
})
</script>

ここまで完了したら、また今までと同様にnpm run devで動作確認してみましょう!

これでmicroCMSとの繋ぎこみ部分まで完了です。後はデプロイだけ!

ここまでの内容をGitHubにpushしたら一旦、休憩したら次に進みましょう!

3. Vercelでデプロイ

3-1. 初期設定、サインアップからプロジェクト作成まで

Vercelのサインアップがまだの方は、Vercelのサイトから「Start Deploying」=>「Continue with GitHub」を選択してください。

下記のようなダッシュボード画面が開いたら、「Import Project」を選んでください。

そしたら下記のような新規プロジェクト作成の画面が開くはずです。「Install」からGitHubのインポートしたいリポジトリを選んでやりましょう。

全てのリポジトリに適用したい時は「All repositories」で。自分の場合は今回のブログのみで選択しました。

無事、選べたら下記のリポジトリ一覧からまた「Import」を選択。

3-2. Deploy設定及びビルド&デプロイ

Deployの設定では「Environment Variables」から先程ローカル開発で.env.localに入れて使っていたmicroCMSのAPIキーを入れた環境変数VITE_HEADLESS_CMS_API_KEYを設定してあげましょう。

「Deploy」を教えて上げたらしばし、ビルド&デプロイタイムを待ちましょう。

上手く行ったみたいです!プレビュー画面をクリックすると、無事サイトにアクセス出来ました!
ここまで、お疲れ様でした!

GitHubで編集を提案

Discussion