Nuxt.jsmicroCMS

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

公開日:2022-04-29 更新日:2023-06-11

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の作成

早速記事一覧の取得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>

記事一覧をcomponent化

少し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>

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

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

Twitter Share