Nuxt.jsmicroCMS

Nuxt3とmicroCMSで作るブログ ③記事詳細ページの作成

公開日:2022-11-26 更新日:2023-06-11

Nuxt3とmicroCMSで作るブログシリーズの3番目の記事です。

前回に記事一覧ページを作成したので、今回は記事詳細ページを作っていきます。

記事詳細APIの作成

前回に引き続き記事詳細もサーバサイドから取得していきます。

server/apiにpostDetail.tsを作成し、以下のようにします。

// src/server/api/postDetail.ts
import client from './client'
import { Post } from '../../types/blog'

export default defineEventHandler(async (event) => {
    const params = getQuery(event)
    const slug = params.slug
    const data = client.getListDetail<Post>({
        endpoint: 'post',
        contentId: String(slug),
    })
    return data
})

まずここの部分で動的に記事のID,つまりslugを取得するようにしています。

const params = getQuery(event)
const slug = params.slug

例えばクライアント側でhttp://localhost:3000/my-pretty-slugにアクセスしたら、my-pretty-slugというパラメータを付与して、APIを呼びます。

記事詳細ページの作成

次にpages内に[slug]ディレクトリを作り、index.vueを作成します。

<!-- src/pages/[slug]/index.vue -->
<script setup lang="ts">

const route = useRoute();
const slug = String(route.params.slug);

const { data: article } = await useFetch(`/api/postDetail`, {
    params: { slug: slug },
});

if (!article.value) {
    throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}

</script>

<template>
    <div class="main">
        <span class="published">{{ $formatDate(article.publishedAt) }}</span>
        <span v-for="(tag, i) in article.tag" :key="tag.id" class="tag">{{ tag.name }}
        </span>
        <h1 class="title">{{ article.title }}</h1>
        <div class="md" v-html="article.text" />
    </div>
</template>

<style scoped>
.main {
    position: relative;
    width: 720px;
    margin: 0 auto 0;
    padding: 112px 0;
    color: #0d1a3c;
}

.published {
    font-size: 1.4rem;
    color: #888;
    margin-right: 20px;
}

.title {
    margin-top: 10px;
    margin-bottom: 30px;
    font-size: 2.4rem;
    color: #0d1a3c;
    line-height: 1.6;
}

.tag {
    font-size: 1.4rem;
    color: 888;
    opacity: 0.7;
    letter-spacing: 1px;
    margin-right: 12em;
}

.md:deep(*) {
    margin-top: 0;
    margin-bottom: 2rem;
    line-height: 1.9;
    font-size: 1.6rem;
    font-weight: 500;
}

.md:deep(strong) {
    background-color: yellow;
}

.md:deep(img) {
    display: block;
    max-width: 100%;
    margin-top: 20px;
    margin-bottom: 0px;
    height: auto;
    border: solid 1px #ccc;
}

@media (max-width: 1024px) {
    .md:deep(img) {
        max-width: 100%;
    }
}

.md:deep(p) code {
    background-color: #eee;
    color: #333;
    padding: 0.2em 0.4em;
    font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
    margin-left: 0.5rem;
    margin-right: 0.5rem;
}

.md:deep(pre) {
    line-height: 1.2;
    background-color: #1D1F21;
    padding: 2% 4%;
    overflow-x: scroll;
}

.md:deep(pre) * {
    margin-bottom: 0;
    line-height: 1.4;
    font-weight: lighter;
}

.md:deep(blockquote) {
    padding: 15px;
    border-left: 5px solid #ccc;
    border-radius: 2px;
}

.md:deep(h1) {
    font-size: 24px;
    border-bottom: 1px solid #ccc;
    font-weight: bold;
    margin-top: 20px;
    margin-bottom: 10px;
}

.md :deep(h2),
.md:deep(h3) {
    font-size: 20px;
    margin-top: 30px;
    margin-bottom: 10px;
    font-weight: bold;
}

.md:deep(h2) {
    border-bottom: 1px solid #ccc;
}

.md:deep(.cp_embed_wrapper) {
    margin-top: 20px;
    margin-bottom: 20px;
}

.md:deep(a) {
    color: #1266f1;
}

.md:deep(a:hover) {
    opacity: .5;
}

.md:deep(ul),
.md:deep(ol) {
    padding-left: 1.5em;
    margin: 1rem 0;
    line-height: 1.7;
}

.md:deep(ul) {
    list-style-type: disc;
}

.md:deep(ol) {
    list-style-type: decimal;
}

.md:deep(ul) li,
.md:deep(ol) li {
    margin-bottom: 1rem;
}
</style>

Nuxt3では[slug]/index.vueという風な階層にすることで、動的ルーティングができます。

このあたりは公式ページが詳しいです。

https://v3.nuxtjs.org/guide/directory-structure/pages/

クライアント側ではここの部分でslugを取り出して、パラメーターを付与してサーバーに投げます。

const route = useRoute();
const slug: string | string[] = route.params.slug;

const { data: article } = await useFetch(`/api/postDetail`, {
  params: { slug: String(slug) },
});

そうして、サーバー側からAPIリクエストを行い、記事が返却される、という流れです。

ここまでで、このように表示がされます。

コードのハイライトを行う

次にせっかくリッチエディタで書いているので、コードのシンタックスハイライトを行っていきましょう。

参考

サーバサイドでシンタックスハイライトを行う

参考記事と同様にcheerioとhighlight.jsを使っていきます。

yarn add cheerio
yarn add highlight.js

<!-- src/pages/[slug]/index.vue -->
<script setup lang="ts">
import * as cheerio from 'cheerio'; //追加
import hljs from 'highlight.js'//追加
import 'highlight.js/styles/hybrid.css'//追加

const route = useRoute();
const slug = String(route.params.slug);

const { data: article } = await useFetch(`/api/postDetail`, {
    params: { slug: slug },
});

if (!article.value) {
    throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}

// hljsクラスをつける
const $ = cheerio.load(article.value.text);
$('pre code').each((_, elm) => {
    const result = hljs.highlightAuto($(elm).text());
    $(elm).html(result.value);
    $(elm).addClass('hljs');
});
const body = $.html()

</script>

<template>
    <div class="main">
        <span class="published">{{ $formatDate(article.publishedAt) }}</span>
        <span v-for="(tag, i) in article.tag" :key="tag.id" class="tag">{{ tag.name }}
        </span>
        <h1 class="title">{{ article.title }}</h1>
        <!-- 変更 -->
        <div class="md" v-html="body" />
    </div>
</template>

script内でuseFetchで取得したデータを取り出す際はarticle.value.textのようにvalueを付ける決まりになっているようです。

ちょっとはまったポイントでした。

次回はトップページのページング処理を行っていきます。

Twitter Share