概要
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/content
にArticle.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>
これで個別のページが完成しました。
記事一覧からリンクを辿っていくと以下のように表示されます。