Nuxt3とmicroCMSで作るブログシリーズの2番目の記事です。
今回はトップページに記事一覧を表示させます。
Nuxt3では、server/apiディレクトリにファイルを追加していくことで、簡単にバックエンドAPIサーバーを作ることができるようになりました。
今回のブログも基本的にサーバーサイドからデータを取得していくようにします。
srcディレクトリ内にserver→apiとディレクトリを作り、hello.tsを作ります。
/* src/server/api/hello.ts */
export default () => 'Hello Server API'
このように配置をすると/api/hello
というエンドポイントでサーバーから値が取得できるようになります。
index.vueを以下のようにします。
<!-- src/pages/index.vue -->
<script setup lang="ts">
const { data: message } = await useFetch('/api/hello')
</script>
<template>
<div>
<h1 style="
font-size:50px;
text-align:center;
margin:100px 0 auto;
">{{ message }}</h1>
</div>
</template>
このようにクライアントサイドから取得できることが分かります。
早速記事一覧の取得APIを作っていきましょう。
TypeScriptを使うので、型をまとめたファイルを作成しておきましょう。
src
ディレクトリにtypes
ディレクトリを作成し、blog.ts
を作成します。
/* src/types/blog.ts */
import type { MicroCMSListContent } from "microcms-js-sdk";
export type Tag = {
name: string;
} & MicroCMSListContent;
export type Post = {
title: string;
tag: Tag[];
text: string;
} & MicroCMSListContent;
microCMSのAPI取得をする際にデフォルトで付与されるフィールドについては、microcms-js-sdk内で事前にtypeの定義がされています。
例えば、記事のidだったり、作成日などですね。
なので、それをimportして組み合わせるようにしています。
次にmicroCMSの記事取得用のサーバークライアントを共通利用するため、server/apiにclient.ts
を作ります。
// src/sever/api/client.ts
import { createClient } from 'microcms-js-sdk'; //ES6
const ctx = useRuntimeConfig();
const client = createClient({
serviceDomain: ctx.serviceDomain,
apiKey: ctx.apiKey,
});
export default client
次にこれらを読み込む形でpostList.ts
を作ります。
// src/server/api/postList.ts
import client from './client'
import { Post } from '../../types/blog'
export default defineEventHandler(async (event) => {
const data = await client
.getList<Post>({
endpoint: 'post',
})
return data
})
この段階でyan devしてhttp://localhost:3000/api/postList
にアクセスしてみましょう。
以下のように記事一覧が表示されます。
次に記事一覧を表示させていきます。
index.vueを以下のようにします。
一覧の表示部分とスタイルを追加しています。
左側に一覧の表示、右側に検索フォームやタグ一覧を表示するレイアウトにします。
<!-- src/pages/index.vue -->
<script setup lang="ts">
const { data: posts } = await useFetch('/api/postList')
</script>
<template>
<div>
<div class="divider">
<section class="container">
<!-- 記事一覧 -->
<article class="article" v-for="post in posts.contents" :key="post.id">
<span class="published">{{ post.publishedAt }}</span>
<span v-for="tag in post.tag" :key="tag.id" class="tag">{{ tag.name }}</span>
<nuxt-link :to="`/${post.id}`">
<h1 class="title">
{{ post.title }}
</h1>
</nuxt-link>
</article>
</section>
<aside class="aside">
<!-- キーワード検索、タグ一覧 -->
</aside>
</div>
</div>
</template>
<style scoped>
.article {
margin-top: 1rem;
margin-bottom: 6rem;
width: 100%;
align-items: center;
}
.published {
font-size: 1.4rem;
color: #888;
margin-right: 20px;
}
.title {
margin-top: 6px;
font-size: 2.8rem;
color: #0d1a3c;
line-height: 1.6;
font-weight: bold;
}
.title:hover {
opacity: .5;
}
.tag {
font-size: 1.4rem;
color: 888;
opacity: 0.7;
letter-spacing: 1px;
margin-right: 1rem;
}
@media (min-width: 1160px) {
.divider {
display: flex;
justify-content: space-between;
width: 1080px;
margin: 20px auto 0;
padding-top: 84px;
}
.container {
width: 600px;
}
.aside {
width: 300px;
}
}
@media (min-width: 820px) and (max-width: 1160px) {
.divider {
margin: 20px auto 0;
width: 740px;
padding-top: 112px;
}
.aside {
margin-top: 60px;
}
}
@media (max-width: 820px) {
.divider {
margin: 20px 0 0;
padding: 0 20px;
padding-top: 112px;
}
.aside {
margin-top: 60px;
width: 100%;
}
}
</style>
少しindex.vueの記述量が多くなってきたので、記事部分をコンポーネント化しておきましょう。
components内にPostList.vueを作成します。
<!-- src/components/PostList.vue -->
<script setup lang="ts">
import { Post } from '../types/blog'
type Props = {
posts: Post[];
}
const { posts } = defineProps<Props>()
</script>
<template>
<div>
<article class="article" v-for="post in posts" :key="post.id">
<span class="published">{{ post.publishedAt }}</span>
<span v-for="tag in post.tag" :key="tag.id" class="tag">{{ tag.name }}</span>
<nuxt-link :to="`/${post.id}`">
<h1 class="title">
{{ post.title }}
</h1>
</nuxt-link>
</article>
</div>
</template>
<style scoped>
.article {
margin-top: 1rem;
margin-bottom: 6rem;
width: 100%;
align-items: center;
}
.published {
font-size: 1.4rem;
color: #888;
margin-right: 20px;
}
.title {
margin-top: 6px;
font-size: 2.8rem;
color: #0d1a3c;
line-height: 1.6;
font-weight: bold;
}
.title:hover {
opacity: .5;
}
.tag {
font-size: 1.4rem;
color: 888;
opacity: 0.7;
letter-spacing: 1px;
margin-right: 1rem;
}
</style>
親コンポーネントからpostsオブジェクトを受け取り表示させるようなイメージです。
Nuxt3ではcompositionAPIをベースにしているので、propsの記述の仕方がNuxt2と変わっています。
Nuxt2では以下のような形でした。
<script>
export default {
props: {
posts: {
type: Object,
},
}
}
</script>
Nuxt3ではこうなっています。
<script setup lang="ts">
import { Post } from '../types/blog'
type Props = {
posts: Post[];
}
const { posts } = defineProps<Props>()
</script>
あとはcomponentをindex.vueで読み込みましょう。
<!-- src/pages/index.vue -->
<script setup lang="ts">
const { data: posts } = await useFetch('/api/postList')
</script>
<template>
<div>
<div class="divider">
<section class="container">
<!-- 記事一覧 -->
<PostList :posts="posts.contents" />
</section>
<aside class="aside">
<!-- キーワード検索、タグ一覧 -->
</aside>
</div>
</div>
</template>
<style scoped>
@media (min-width: 1160px) {
.divider {
display: flex;
justify-content: space-between;
width: 1080px;
margin: 20px auto 0;
padding-top: 84px;
}
.container {
width: 600px;
}
.aside {
width: 300px;
}
}
@media (min-width: 820px) and (max-width: 1160px) {
.divider {
margin: 20px auto 0;
width: 740px;
padding-top: 112px;
}
.aside {
margin-top: 60px;
}
}
@media (max-width: 820px) {
.divider {
margin: 20px 0 0;
padding: 0 20px;
padding-top: 112px;
}
.aside {
margin-top: 60px;
width: 100%;
}
}
</style>
次に日付表記を見やすくしていきます。
microCMSの日付データはISO 8601形式のUTC(協定世界時)にて返却されるので、これを日本時間に直して表示します。
下の記事を参考にdayjsを使っていきます。
http://help.microcms.io/ja/knowledge/specification-of-utc-time
yarn add dayjs
Nuxt3ではpluginsディレクトリからprovideすることでヘルパー関数が作れるようです。
pluginsディレクトリにplugin.tsを作成し、以下のようにします。
// src/plugins/plugin.ts
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
export default defineNuxtPlugin(() => {
return {
provide: {
/* 日付表記を直す */
formatDate: (inputDate: string) => {
dayjs.extend(utc);
dayjs.extend(timezone);
return dayjs.utc(inputDate).tz('Asia/Tokyo').format('YYYY-MM-DD')
},
}
}
})
これをテンプレートから呼び出すには以下のようにします。
<!-- src/components/PostList.vue -->
<script setup lang="ts">
import { Post } from '../types/blog'
type Props = {
posts: Post[];
}
const { posts } = defineProps<Props>()
</script>
<template>
<div>
<article class="article" v-for="post in posts" :key="post.id">
<!-- ここ -->
<span class="published">{{ $formatDate(String(post.publishedAt)) }}</span>
...
</article>
</div>
</template>
このように日付表記が直りました。
次回は記事詳細ページの表示を行います。