Nuxt 3 + Nuxt Content v2 による静的サイトの作成
はじめに
本ブログは、タイトルにある Nuxt 3 と Nuxt Content v2 を用いて作成しています。
もともとは Nuxt 2 + Nuxt Content で作成していたのですが、Vue 2 のEOLが2023年末ということで Nuxt 3 に移行しました。 移行といっても、実際は一から作り直しています…。 まぁデザインも変えたりしたかったので、一から作り直してよかったとも思っています。 コード量も大したことないので。
とはいえ、Nuxt 3 や Nuxt Content が v2 になったことで苦労したことがどうしてもあります。 これら踏まえて、Nuxt 3 + Nuxt Content v2 の静的サイトの作成方法について説明をしていきます。
プロジェクトの作成
以下コマンドで Nuxt 3 のプロジェクトを作成とパッケージのインストールをします。
> npx nuxi init <project-name>
> cd <project-name>
> npm install
そして、以下コマンドでNuxt Contentをインストールします。
> npm install @nuxt/content
インストールが完了したら、nuxt.config.js(ts)
のmodules
に追加します。
export default defineNuxtConfig({
modules: ['@nuxt/content'],
})
最低限であればこれで大丈夫です。
今回使用しているバージョンは以下の通りです。
nuxt: 3.7.0
@nuxt/content: 2.7.2
ページ構成
このブログではpages/
ディレクトリは以下の構成になっています。
├ post/
│ └ [...slug].vue
├ index.vue
└ policy.vue
[...slug].vue
が各記事のページ(動的ページ)となっています。
Nuxt 3 から動的ページは[]
で表現するようになっており、さらに[...]
とすることで下位のすべての階層をターゲットにすることができます。
今回の場合、post/
はなくても動作しますが、意図的に作成しています。
さて、ここで注意が必要なのは、index ページにすべてのページのリンクを作成しなければならないということです。
これをしなければ、npm run generate
をした場合に動的ページが生成されません。
ここが今回のハマりポイントの1つでした。
具体的には、NuxtLink
ですべての記事へのリンクを作成する必要があります。
例えばcomputed
でリストにフィルターをかけたり、v-if
で一部リンクを非表示にするといったことをした場合に、初期表示で非表示となる(レンダリングされない)ページは生成されません。
もしかすると他にも良い方法があるかもしれないのですが、残念ながら見つけることはできませんでした。 他の方法があればご教示いただきたいです。
Nuxt Content
Nuxt Content については以下が公式ドキュメントとなります。 この中から今回利用したものだけをピックアップして説明します。
コンテンツ作成
Nuxt Content では、プロジェクト直下のcontent/
ディレクトリ内に作成した md ファイルがページのコンテンツとなります。
コンテンツはMarkdownで記述するため、開発者であればある程度簡単に作成することができると思います。
また、content/
のディレクトリ階層はそのままページのパスとなります。
このページであれば、content/post/nuxt3/nuxt3-blog.md
というファイルがページコンテンツとなっています。
コンテンツを作成するときは、どのようなパスになるかを考えて作成してみてください。
基本的には通常のMarkdownの記述なのですが、プラスでコンテンツ情報を設定することができます。
コンテンツ情報は、ファイルの先頭の---
で囲った部分に以下のようにパタメータとして設定します。
---
title: 'コンテンツのタイトル'
description: 'コンテンツの詳細'
---
〜 ここから本文 〜
#
(<h1>
)は、上記のようにtitle
として設定するため使用しません。
コンテンツ情報として設定できる項目は以下を参考にしてください。
https://content.nuxtjs.org/guide/writing/markdown#native-parameters
実際には上記にない項目も自由に設定することが可能です。
---
title: 'コンテンツのタイトル'
description: 'コンテンツの詳細'
tags: ['Nuxt 3']
img: 'nuxt3.svg'
---
〜 ここから本文 〜
コンテンツ情報
コンテンツの取得の前に、コンテンツが持つ情報を示しておきます。
コンテンツの情報はParsedContentInternalMeta
で定義されており、以下の項目を持ちます。
export interface ParsedContentInternalMeta {
_id: string
_source?: string
_path?: string
title?: string
_draft?: boolean
_partial?: boolean
_locale?: string
_type?: string
_file?: string
_extension?: string
}
これに加えて上記で設定したtags
のようなコンテンツ情報を取得できます。
コンテンツ取得
コンテンツの取得方法には、Composable を使用する方法と Component を使用する方法の2つがあります。 どちらを使用してもおそらく問題はないので、好きな方を選択すれば良いかと思います。
queryContent()
1つはComposableとして定義されているqueryContent()
を使用します。
queryContent()
の引数には、content/
ディレクトリの中で取得したいディレクトリを指定します。
// /content 以下すべてのコンテンツを取得
const contentQuery = queryContent()
// /content/articles 以下すべてのコンテンツを取得
const contentQuery = queryContent('articles')
// /content/articles/nuxt3 以下すべてのコンテンツを取得
const contentQuery = queryContent('articles', 'nuxt3')
取得するコンテンツを絞りたい場合はwhere()
、並べ替えたい場合はsort()
を使用します。
終端処理として、取得するコンテンツが複数の場合はfind()
、1つの場合はfindOne()
を使用します。
// 複数
const articles = queryContent('articles').sort([{ createdAt: -1 }]).find()
// 単体
const article = queryContent('articles').where({_path: path}).findOne()
その他メソッドや使用方法の詳細などについては、公式ドキュメントを参考にしてください。
実際に取得する場合は、useAsyncData()
を用います。
<script setup>
const { path } = useRoute()
const { data } = await useAsyncData(`content-${path}`, async () => {
const article = queryContent().where({ _path: path }).findOne()
return {
article: await article,
}
})
</script>
ContentList
<ContentList>
は Component として定義されており、path
とquery
を指定することで複数のコンテンツを取得します。
query
は、queryContent()
で使用したwhere()
やsort()
を指定し、コンテンツを絞ったりします。
<template>
<ContentList path="/articles" :query="{ sort: [{ createdAt: -1 }] }">
<template #default="{ list }">
<NuxtLink v-for="article in list" :key="article._path" :to="article._path">
{{ article.title }}
</NuxtLink>
</template>
<template #not-found>
<p>No articles found.</p>
</template>
</ContentList>
</template>
ContentDoc
<ContentDoc>
は Component として定義されており、現在のパスに一致する単一コンテンツを取得します。
<template>
<ContentDoc v-slot="{ doc }">
<!-- ページ -->
</ContentDoc>
</template>
コンテンツ表示
基本的には、コンテンツ情報で示した項目を使用してデザインしていきます。
Markdownで記述した本文については、<ContentRender>
を用いることでHTMLに変換されます。
<template>
<ContentDoc v-slot="{ doc }">
<article>
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" />
</article>
</ContentDoc>
</template>
参照: https://content.nuxtjs.org/api/components/content-renderer
内部的には Prose Components によってHTMLに変換されます。 次に説明しますが、対応するコンポーネントと同名のコンポーネントを作成することで、どのように変換するかカスタマイズすることが可能です。
シンタックスハイライト
テーマの設定
Nuxt Content v2 では、shiki-es でシンタックスハイライトが設定されています。 利用できるテーマは以下になります。
css-variables | dark-plus | dracula-soft | dracula | github-dark-dimmed |
github-dark | github-light | hc_light | light-plus | material-theme-darker |
material-theme-lighter | material-theme-ocean | material-theme-palenight | material-theme | min-dark |
min-light | monokai | nord | one-dark-pro | poimandres |
rose-pine-dawn | rose-pine-moon | rose-pine | slack-dark | slack-ochin |
solarized-dark | solarized-light | vitesse-dark | vitesse-light |
一部テーマは以下サイトより確認することができます。
テーマは、nuxt.config.js
のcontent.highlight.theme
に設定します。
1パターンのみの場合はテーマの文字列を、ダークモードを使用する場合は以下のようなオブジェクトを指定します。
export default defineNuxtConfig({
content: {
highlight: {
//単一
theme: 'github-light',
//モード別
theme: {
default: 'light-plus',
dark: 'dark-plus'
}
},
},
})
デザインのカスタマイズ
<code>
は以下のようにファイル名やハイライトさせる行数を指定することができます。
```js [file.js]{4-6,7}
export default () => {
console.log('Code block')
}
```
しかし、Nuxt Content v2 のデフォルトではファイル名を表示させることができません。 これを解決するために先ほど示した Prose Components を使用します。
<code>
には<ProseCode>
が対応しており、以下のようにプロジェクトのコンポーネントとして同名のファイルを作成することで上書きします。
<template>
<div class="prose-code">
<div class="prose-code__file" v-if="filename">
{{ filename }}
</div>
<slot />
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
code?: string
language?: string | null
filename?: string | null
highlights?: number[]
}>(),
{ code: '', language: null, filename: null, highlights: undefined }
)
</script>
<slot />
に<code>
の内容が展開されます。
あとは対応するプロパティを基にデザインを作成します。
このブログではやっていませんが、言語を表示したり、クリップボードへのコピーボタンをつけたりすることも可能です。
参考: https://mokkapps.de/blog/how-to-create-a-custom-code-block-with-nuxt-content-v2
ページのデザイン
デザインについてはおまけのようなものですが、一応簡単に今回使用したものについてご紹介しておきます。
Tailwind CSS
普段はCSSフレームワークは使用せずプレーンのままゴリゴリ書いていくのですが、今回はせっかくなのでTailwind CSSを使用してみました。
Nuxt 用に@nuxtjs/tailwindcss
があるのでこちらを使用します。
> npm install -D @nuxtjs/tailwindcss
> npx tailwindcss init
export default defineNuxtConfig({
modules: ['@nuxt/tailwindcss'],
})
npx tailwindcss init
により、プロジェクトディレクトリーにtailwind.config.js
が生成されます。
このファイルにTailwind CSSの設定を記述していきます。
またassets/css/
にtailwind.css
を以下の内容で作成します。
これで準備は完了です。
@tailwind base;
@tailwind components;
@tailwind utilities;
Tailwind CSSの詳細については触れませんが、class名によりデザインを適用することができます。 例えば以下は背景を黒色、文字を白色に設定しています。
<div class="bg-black text-white">
hogehoge
</div>
また@apply
を用いることでCSSでまとめてデザインを適用することができます。
<template>
<div class="box1">
hogehoge
</div>
</template>
<style scoped>
.box1 {
@apply bg-black text-white;
}
</style>
アイコン
アイコンはtailwindcss-icons
を使用しています。
一緒に@iconify/json
をインストールする必要があります。
> npm install -D @egoist/tailwindcss-icons @iconify/json
使用するにはtailwind.config.js
に以下を追記します。
const { iconsPlugin, getIconCollections } = require("@egoist/tailwindcss-icons")
module.exports = {
plugins: [
iconsPlugin({
collections: getIconCollections(["mdi", "lucide"]),
}),
],
}
getIconCollections()
に使用するアイコンのライブラリを指定します。
使用できるアイコンは以下から検索することができます。
アイコンを表示するには以下のようにi-
から始まる名前をclass
に設定します。
上記サイトで表示したいアイコンをクリックすれば名前が表示されるため。ケバブケース(-
繋ぎ)の名前をコピーして使用します。
<span class="i-mdi-home"></span>
ダークモード
このブログでは、ダークモードを実装するためにTailwind CSS と @nuxtjs/color-mode
を使用しています。
詳細は割愛しますが、以下のように各設定ファイルに追記します。
> npm install -D @nuxtjs/color-mode
export default defineNuxtConfig({
modules: ['@nuxt/tailwindcss', '@nuxtjs/color-mode'],
colorMode: {
classSuffix: '',
},
})
module.exports = {
darkMode: 'class'
}
デザイン方法はいたってシンプルで、dark:
を先頭に付与したものがダークモードのデザインとして適用されます。
例えば以下は、通常時は白色背景に黒文字、ダークモードでは黒色背景に白文字としています。
<div class="bg-white text-black dark:bg-black dark:text-white">
hogehoge
</div>
肝心のテーマの切り替え方法ですが、以下のようにcolorMode
のpreference
の値を変更します。
現在のテーマを調べる場合もpreference
の値を参照すればよいです。
const colorMode = useColorMode()
const changeLightMode = () => {
colorMode.preference = 'light'
}
const changeDarkMode = () => {
colorMode.preference = 'dark'
}
ただし、preference
の初期値はsystem
になっており、初回表示時は端末(OS)のモードによって決まります。
このとき、どちらのモードであるかは、colorMode.value
を参照してください。
記事のデザイン
記事のデザインについてはもうゴリゴリに自分で描いています。 MarkdownとHTMLの関係がわかっていれば、何に対してデザインを設定するかは難しくないと思います。 先述しましたが、ものによっては Prose Components を書き換えるというのも1つ手だと思います。
静的サイトの生成、デプロイ
静的サイトを生成する場合は、npm run generate
を使用します。
うまくいけば.output/public
またはdist
に生成されます。
できたサイトを確認したい場合は、npm run preview
を実行します。
デプロイについては割愛しますが、このブログは Netlify を使用しており、GithubへのPushをトリガーにビルド、デプロイを行なっています。
ただ1点注意が必要で、場合によってはメモリオーバーでビルドが失敗します。
実際にこのブログをデプロイする際も、ビルドで失敗してハマりました。
解決方法としては、以下のように generate
を書き換えることで使用するメモリの最大値を制限します。
"generate": "NODE_OPTIONS=--max_old_space_size=8192 nuxt generate"
その他の設定
サイトマップ
このブログでは、サイトマップの作成にnuxt-simple-sitemap
を使用しています。
> npm install -D nuxt-simple-sitemap
export default defineNuxtConfig({
modules: ['nuxt-simple-sitemap'],
})
この設定だけで、サイト生成時にsitemap.xml
が生成されます。
Google Analytics
Google Analyticsを設定するために、Nuxt Gtagを使用します。
> npm install -D nuxt-gtag
以下のように設定ファイルにid
を設定します。
export default defineNuxtConfig({
modules: ['nuxt-gtag'],
gtag: {
id: 'G-XXXXXXXXXX',
},
})
おわりに
非常に簡単ではありますが、このブログを作り直すためにやったことについて説明していきました。 感想としては、簡単になっていて良いと思う部分もあり、どう動くか理解できていなく不便だと思う部分もあるって感じです。 まぁ結局は慣れなのかもしれないですが。
正直なところまだ妥協している点や動きやデザインがおかしなところがあったりします。 これらは時間があるときに少しずつ修正していきたいと思います。