iTranslated by AI
[Deploy in Just a Few Hours!] An Easy Roadmap to Building a Blog with Vue.js and microCMS and Publishing It on Vercel
0. Introduction

0-1. What is this article?
This is an entry for the 8th day of the LAPRAS Advent Calendar! (I'm very late... I'm sorry!)
If you follow this article, you can quickly create an original blog using Vue.js and microCMS after work and even deploy it on Vercel.
So, why were you late for the Advent Calendar if it finishes so quickly?
The target audience is as follows:
- You have a GitHub account and use it normally.
- You have experience with JS/TS and Vue.js.
This level is perfectly fine.
Even if you're thinking, "What's a Headless CMS?" or "I've never used a web hosting service like Vercel or Netlify," don't worry!
Furthermore, if you use it at the level described in this article, both microCMS and Vercel can be used for free!
0-2. Technologies and Terms Used
Frontend: Vue.js, NES.css
For the frontend, we will use Vue.js version 3.
Since I wanted to give the blog we're making this time an 8-bit, Famicom-style design, I decided to use a CSS framework called Nes.css.
Later on, I'll explain how to install Nes.css and a matching Japanese font.
Headless CMS: microCMS
First of all, what is a Headless CMS? As the name suggests, it is a "Content Management System (CMS) without a frontend (head)."
Conversely, a typical CMS (with a head) would be something famous like WordPress.
In a Headless CMS, only the API is provided, which allows you to use whatever technology you like for displaying the content. (There are various other benefits, but I'll skip them here.)
microCMS is one such Headless CMS.
Since it's made in Japan, the Japanese documentation is extensive, and the API management screen is clean and easy to use.
Deployment Hosting Service: Vercel
Vercel is a service that combines CI/CD with a web server, making it easy to deploy applications by linking with repositories like GitHub.
While it (apparently) goes well with Next.js, it also supports Vue.js.
Other hosting services often mentioned as comparisons would be Netlify or GitHub Pages.
0-3. Preparation
In this section, we will prepare the Vue.js project and install the necessary libraries using npm.
First, let's set up the Vue.js project.
In this article, I am using Vue.js version 3.8.
Running npm init vue@{{specified_version}}} will prompt you with setup questions, so please proceed as follows.
$ npm init vue@latest
Need to install the following packages:
create-vue@3.8.0
Ok to proceed? (y) y
✔ Project name: … {{ Your preferred blog name }}
✔ Package name: … Default is 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 {{ Created blog directory }}
$ npm install
Next, we will install the necessary libraries.
As for nes.css, I'm only using it to create an 8-bit UI for my blog this time, so please decide whether to install it based on your preference.
-- CSS framework used to create a retro Famicom game-style UI
$ npm install nes.css
-- HTTP client (necessary later for calling the API provided by microCMS)
$ npm install axios
-- Vue UI framework (installing this as it's convenient for creating card layouts)
$ npm install element-plus --save
Finally, please download the Japanese font used this time, Nu Kinako Mochi, from the Booth link and extract it using unzip.
Once this is done, let's check if the following screen starts up.
$ 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. Creating a blog that looks and works locally with Vue.js for now
Ultimately, we aim to reach the state shown in the directory tree below.
$ 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
Since we haven't prepared the API using a Headless CMS (microCMS) yet, we'll focus here on creating a blog that works locally using temporary hard-coded data.
1-1. Making the installed font available + Creating an icon
Create a directory named fonts under src/assets and place the NuKinakoMochi-Reg.otf you downloaded earlier inside it.
Next, delete all the default content in main.css, also located under src/assets, and write the custom font settings using @font-face as follows:
@font-face {
font-family: "NuKinakoMochiRegFont";
src: url("./fonts/NuKinakoMochi-Reg.otf") format("opentype");
}
Now you can use the font under the name NuKinakoMochiRegFont.
While you're at it, please add an image named fire-icon.png to the assets.
By the way, I asked ChatGPT to generate the icon image for me.

1-2. Enabling vue-router, nes.css, and element-plus
As for vue-router, if you included it when running npm init vue@latest, it should be ready to use by default. However, for nes.css and element-plus, you need to configure src/main.ts as follows.
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 './robot'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
1-3. Implementing the Home Screen
We achieve SPA (Single Page Application) by utilizing Vue Router, one of Vue.js's plugins.
First, we'll create the part for switching views via navigation.
App.vue Implementation
<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>Kumamoto Blog</h1>
</div>
</RouterLink>
<div class="nav">
<RouterLink to="/" >
<button class="nes-btn">Blog</button>
</RouterLink>
<RouterLink to="/about">
<button class="nes-btn">Profile</button>
</RouterLink>
</div>
</header>
<div class="main-content">
<RouterView />
</div>
<div class="footer">
This is the 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>
We'll configure routing control under src/router/index.ts.
Let's write it referring to the default syntax.
ArticleView will be implemented later.
Implementing routing control in 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
Next, we'll implement HomeView, which is shown when on the home screen, and the card components that will be listed within it.
For HomeView, let's just delete the default content of HomeView.vue in src/views and use it as is.
HomeView Implementation
<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: Temporary data until we fetch data from the API
blogList.value = [
{
articleId: 'id3',
title: 'Story about being caught going to a meeting after riding an exercise bike',
imgUrl: 'https://sakidorico.s3.amazonaws.com/wp/wp-content/uploads/2023/09/64f1787f8af60.jpg',
content: '<h1>Story about being caught going to a meeting after riding an exercise bike</h1>',
publishedAt: '2023/12/09',
},
{
articleId: 'id2',
title: 'Let me introduce the sauna I love!!',
imgUrl: 'https://manager.reservation.jp//cmn/img/facility/10000001/M/1598601781785219.jpg',
content: '<h1>Let me introduce the sauna I love!!</h1>',
publishedAt: '2023/12/08',
},
{
articleId: 'id1',
title: 'About this blog',
content: '<h1>About this blog</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>
Regarding the card component, please create a new BlogCard.vue file under the src/components directory.
Implementing the card component (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>
Finally, create the ArticleView.vue file under the src/views directory.
For now, a temporary content like the following is fine.
Implementing the component for each article (ArticleView.vue)
<script setup lang="ts">
</script>
<template>
<h1>This is a test</h1>
</template>
<style lang="scss" scoped>
</style>
At this point, run npm run dev. If a blog with the following appearance is created, you're all set! Let's take a coffee break!
-
Localhost root (Home screen)

-
Links to each article

1-4. Implementing Article Content and the Profile Page
Now, let's write the content for ArticleView.vue, which we implemented roughly earlier.
That said, to keep things simple this time, we'll use an easy implementation using the built-in Vue.js directive v-html.
Note: Of course, using v-html in areas where users can input text is dangerous due to the risk of XSS (Cross-Site Scripting), so please avoid doing so in production.
As written in the TODO comment below, we will change this later to actually call the API.
Implementing the content of 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: Define the type later
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: Fetch from API later
blog.value = {
title: 'Story about being caught going to a meeting after riding an exercise bike',
ogpUrl: 'https://sakidorico.s3.amazonaws.com/wp/wp-content/uploads/2023/09/64f1787f8af60.jpg',
content: '<h1>Story about being caught going to a meeting after riding an exercise bike</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>
Next, we will implement the profile page.
For simplicity, we'll make this an easy specification that just lists SNS links.
Note that thanks to nes.css, you can easily introduce 8-bit style icons for SNS links.
We will reuse the default src/views/AboutView.vue by clearing its contents entirely.
Implementing the content of AboutView.vue
<template>
<div class="profile">
<p class="nes-balloon from-left nes-pointer is-dark">
Wooooooooooo!! Follow me!!!
</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>
Once you've implemented this far, run npm run dev again and check the operation just as before.
-
Links to each article

-
Profile page

Now you've created a blog that looks and works locally. Great job!
2. Integration with microCMS
2-1. From microCMS registration to service registration and API setup
If you haven't registered with microCMS yet, please sign up.
After registering, select "Create Service" and then "Create from scratch".

Decide on a "Service Name" and "Service ID" on the screen below.
As mentioned, these can be changed later, so any names are fine.

Once you've decided on the service name, the next step is the API setup.
Since we're building a blog, simply select the blog template as shown below.

If the following API list screen appears, you're all set! It's easy and effortless!

2-2. Confirming and Creating the API
Try clicking "API Preview" in the list we just saw.
You can check how to call the API in various languages and commands, and see what results are returned. In the example below, it seems to be an API that returns a list of articles.

Now, let's actually write an article using "Create API" or "Copy Content".

2-3. Obtaining and Setting the API Key
Once you've written a few articles, the next step is to set up the API key so that you can call the API from the local Vue.js blog you created earlier.
First, please obtain the API key from the microCMS service settings.
(Be careful with its handling to ensure it doesn't leak externally!)

Next, set the environment variable in a .env.local file directly under your local blog directory as follows: ()
export VITE_HEADLESS_CMS_API_KEY=xxxxx
Note that you must prefix the environment variable with VITE_.
Reference: Vite: Env Variables and Modes
2-4. Actually Calling the API
Create a directory and file structure under src as shown below.
├── src
│ ├── api
│ │ ├── articles
│ │ │ └── index.ts
│ │ └── home
│ │ └── index.ts
Next, we will implement the HTTP client using axios in each index.ts.
API for the home screen (src/api/home/index.ts)
import axios from "axios";
class Get {
public async execute() {
// Get the list of articles
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();
API for the article screen (src/api/articles/index.ts)
import axios from "axios";
class Get {
public async execute(articleId: string) {
// Get a single specific article
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();
Now, let's make the Get classes implemented above available in HomeView.vue and ArticleView.vue.
First, starting with HomeView.vue.
Delete the hard-coded blogList portion we wrote earlier and rewrite it as follows:
<script setup lang="ts">
import Get from "@/api/home/index"
onMounted(async () => {
blogList.value = await Get.execute()
})
</script>
Next, for ArticleView.vue. Import api/articles/index here as well and rewrite the hard-coded portion in 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>
Once this is done, let's check the operation using npm run dev just as before!
This completes the integration with microCMS. All that's left is deployment!
Once you've pushed these changes to GitHub, take a break before moving on to the next step!
3. Deployment with Vercel
3-1. Initial Setup: From Signing Up to Project Creation
If you haven't signed up for Vercel yet, go to the Vercel website and select "Start Deploying" => "Continue with GitHub".
Once the dashboard screen like the one below opens, select "Import Project".

Then, a screen for creating a new project should open as shown below. Choose the GitHub repository you want to import from "Install".

If you want to apply it to all repositories, select "All repositories". In my case, I selected only the repository for this blog.

Once you've successfully selected it, choose "Import" again from the repository list below.
3-2. Deployment Settings and Build & Deploy

In the deployment settings, configure the environment variable VITE_HEADLESS_CMS_API_KEY under "Environment Variables" using the microCMS API key you previously put in .env.local for local development.
After clicking "Deploy," wait a bit for the build and deployment process.

It looks like it was successful! Clicking on the preview screen, I was able to access the site without any issues!
Great job getting this far!

Discussion