Qlitre's Blog

2022.04.29 Nuxt.js /microCMS

Nuxt3とmicroCMSで作るブログ ②記事一覧の表示

Nuxt3とmicroCMSで作るブログシリーズの2番目の記事です。
今回はトップページに記事一覧を表示させます。

Nuxt3では、server/apiディレクトリにファイルを追加していくことで、簡単にバックエンドAPIサーバーを作ることができるようになりました。
今回のブログも基本的にサーバーサイドからデータを取得していくようにします。

簡単なテスト

clientディレクトリ内にserver→apiとディレクトリを作り、hello.tsを作ります。

/* client/server/api/hello.ts */
export default (req, res) => 'Hello Server API'


このように配置をすると/api/helloというエンドポイントでサーバーから値が取得できるようになります。

index.vueを以下のようにします。

<!-- client/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の作成

早速記事一覧の取得APIを作っていきましょう。
前準備としてTypeScriptを使うので、取得するコンテンツの型をまとめたtypes.tsを作ります。

// client/server/api/types.ts
export type Tag = {
  id: string
  name: string
}

export type Post = {
  id: string
  title: string
  publishedAt: string
  tag: Array<Tag>
  text: string
}


次にmicroCMSの記事取得用のサーバークライアントを共通利用するため、client.tsを作ります。

// client/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を作ります。

// client/server/api/postList.ts
import type { IncomingMessage, ServerResponse } from 'http'
import client from './client'
import { Post } from './types'

export default async (req: IncomingMessage, res: ServerResponse) => {
    const queries = {
        fields: 'id,title,publishedAt,tag',
    }

    const data = client.getList<Post>({
        endpoint: 'post',
        queries: queries
    })

    return data
}


この段階でnpm run devしてhttp://localhost:3000/api/postListにアクセスしてみましょう。
以下のように記事一覧が表示されます。


記事一覧の表示

次に記事一覧を表示させていきます。
index.vueを以下のようにします。

一覧の表示部分とスタイルを追加しています。
左側に一覧の表示、右側に検索フォームやタグ一覧を表示するレイアウトにします。

<!-- client/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>




記事一覧をcomponent化

少しindex.vueの記述量が多くなってきたので、記事部分をコンポーネント化しておきましょう。
components内にPostList.vueを作成します。

<!-- client/components/PostList.vue -->
<script setup lang="ts">
const props = defineProps({
  posts: Object
})
</script>

<template>
  <div>
    <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>
  </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">
const props = defineProps({
  posts: Object
})
</script>


あとはcomponentをindex.vueで読み込みましょう。

<!-- client/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" />
            </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

npm i dayjs


Nuxt3ではpluginsディレクトリからprovideすることでヘルパー関数が作れるようです。
pluginsディレクトリにplugin.tsを作成し、以下のようにします。

// client/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')
            },
        }
    }
})


これをテンプレートから呼び出すには以下のようにします。

<!-- client/components/PostList.vue -->
<script setup lang="ts">
const props = defineProps({
    posts: Object
})
</script>

<template>
    <div>
        <article class="article" v-for="post in posts.contents" :key="post.id">
            <!-- ここ -->
            <span class="published">{{ $formatDate(post.publishedAt) }}</span>
            ...
        </article>
    </div>
</template>




このように日付表記が直りました。

次回は記事詳細ページの表示を行います。

TOPページへ