概要

Nuxt 3 で Markdown を利用したブログサイトを作成します。

今回作成する完成形はこちら

環境構築

Nuxt プロジェクトの作成には以下のコマンドを実行します。

npx nuxi init nuxt-blog -t content

完了したら作成されたフォルダへ移動してパッケージをインストールします。

yarn devでローカルサーバーが立ち上がります。
ブラウザでhttp://localhost:3000にアクセスしてNuxtのページが表示されていればOKです。

cd nuxt-blog
yarn install
yarn dev

トップページ

記事の作成

Markdown で記事を作成します。
新しくcontent/postsというフォルダを作成し、記事はそこに置くようにします。

mkdir content/posts

フォルダ内に.mdファイルを作成して記事を書きます。

touch content/posts/my-first-blog-post.md

以下が Markdown ファイルの中身です。
適当なタイトルと内容で問題ありません。

記事では画像も使っているのでUnsplashなどでダウンロードし、publicフォルダにimage1.jpegの名前で置いています。

---
title: こころ
date: 2022-08-28
image: image1.jpeg
---

## 先生と私

私わたくしはその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚はばかる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執とっても心持は同じ事である。よそよそしい頭文字かしらもじなどはとても使う気にならない。

ローカルサーバーを立ち上げ直してhttp://localhost:3000/posts/my-first-blog-postへアクセスします。
すると先ほど書いた記事が見られるようになっています。

先生と私

Tailwind

これからもっとブログらしくなるように手を加えていきます。
ページのスタイリングにはTailwindというCSSフレームワークを使用します。

Nuxt 3にはTailwindのプラグインが用意されているのでそれをインストールします。

yarn add --dev @nuxtjs/tailwindcss

nuxt.config.tsを開いて次のように@nuxtjs/tailwindcssを追加します。

import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  modules: [
    '@nuxt/content',
    '@nuxtjs/tailwindcss'
  ]
})

ヘッダーの作成

ここからページのそれぞれの箇所を作っていきます。
まずは、サイトのヘッダーから。

componentsというフォルダを作成して、その中にHeader.vueの名前でファイルを作成します。

mkdir components
touch components/Header.vue

Header.vueのコードは以下のとおりです。

<template>
  <header class="bg-gray-100 p-2 mb-10">
    <div class="max-w-5xl m-auto flex justify-between">
      <div class="text-xl font-bold">
        COYOTE
      </div>
      <div>
        <ul class="flex">
          <li class="mr-4">
            <NuxtLink to="/" class="hover:text-sky-500">Home</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/about" class="hover:text-sky-500">About</NuxtLink>
          </li>
        </ul>
      </div>
    </div>
  </header>
</template>

ページに表示されるようにapp.vueを編集します。
これでヘッダー部分の完成です。

<template>
  <div>
    <Header />
    <NuxtPage />
  </div>
</template>

ブラウザで確認すると以下のようになっています。

ヘッダー

記事一覧ページ

次に記事一覧ページを作成します。

一覧表示にはカード型のレイアウトを採用することにします。
よってまずはその構成要素となるCardコンポーネントを作成します。

componentsフォルダ下にcontentフォルダを新しく作り、その中でCard.vueという名前でファイルを作成してください。

mkdir components/content
touch components/content/Card.vue

Cardコンポーネントでは記事のメタデータをプロパティとして受けとって表示するという形をとることにします。
以下がCard.vueのコードになります。

<script setup>
const props = defineProps({
  title: String,
  date: String,
  image: String,
  path: String
})

const formatDate = computed(() => {
  const d = new Date(props.date)
  return `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}`
})
</script>

<template>
  <div
    class="
      border border-solid border-slate-100
      w-full
      mr-0
      mb-4
      md:w-[31.707%] md:mr-[2.43902%] md:[&:nth-child(3n)]:mr-0
    "
  >
    <div class="relative pt-[56.25%] overflow-hidden">
      <img class="absolute w-full top-2/4 translate-y-[-50%]" :src="image" />
    </div>
    <div class="flex flex-col px-2 py-4">
      <NuxtLink class="text-xl font-medium hover:text-sky-500" :to="path">
        {{ title }}
      </NuxtLink>
      <span class="text-sm text-stone-400">{{ formatDate }}</span>
    </div>
  </div>
</template>

次に記事の情報を取得してCardコンポーネントに渡す役割をするCardListコンポーネントを作成します。
components/contentフォルダにCardList.vueファイルを作ります。

touch components/content/CardList.vue

記事の情報の取得はqueryContent()を使って行います。
今回、記事のファイルはpostsフォルダ下にあるので引数にpostsを渡しています。

dataにはpostsフォルダにある記事データが配列として格納されます。
v-forでリストレンダリングをして複数の記事をまとめて表示されるようにします。

<script setup>
const { data } = await useAsyncData('home', () => queryContent('posts').find())
</script>

<template>
  <div class="max-w-5xl m-auto px-2">
    <h2 class="text-2xl font-semibold mb-4">Recent Posts</h2>
    <div class="flex flex-wrap">
      <Card
        v-for="post in data"
        :title="post.title"
        :date="post.date"
        :image="post.image"
        :path="post._path"
      />
    </div>
  </div>
</template>

このCardListをトップ画面に表示させます。 それにはcontent/index.mdを開いて中身を全て削除してから次の一文に置き換えます。

:card-list

ここまでで、ブラウザにアクセスしてみると以下のようになっています。

記事一覧

記事詳細ページ

一覧ページができたので個別の記事ページを作っていきます。
個別の記事はArticleコンポーネントとして実装することにします。

components/contentArticle.vueファイルを作成してください。

touch components/content/Article.vue

以下のコードがArticle.vueの内容になります。

デフォルトでは表示されない記事のタイトルやヘッダーイメージを追加しています。
また、styleタグ内で見出しのフォントサイズやテキストの間隔を調整しています。

<template>
  <div class="max-w-5xl m-auto">
    <ContentDoc v-slot="{ doc }">
      <img class="w-full h-auto max-h-96 object-cover" :src="`/${doc.image}`" />
      <div>
        <h1>{{ doc.title }}</h1>
        <ContentRenderer :value="doc" />
      </div>
    </ContentDoc>
  </div>
</template>

<style scoped>
:deep(h1) { @apply text-4xl my-8; }
:deep(h2) { @apply text-2xl my-4; }
:deep(p) { @apply leading-7; }
</style>

Articleコンポーネントで記事ページを表示させるために、もう一つページ用のコンポーネントを作成します。
pagesフォルダに新しくpostsフォルダを作成してその中に[[slug]].vueという名前でファイルを用意します。

mkdir pages/posts
touch pages/posts/[[slug]].vue

内容は以下のとおりです。

<template>
  <Article />
</template>

これで個別のページが完成しました。
記事一覧からリンクを辿っていくと以下のように表示されます。

記事詳細