📝

Nuxt 3 / Content で Markdown なブログ・お知らせサイト作る

2023/11/14に公開

昔書いた記事のアップデート版です。Nuxt 3 で Nuxt Content 周りも結構 API が変わったのでその辺が注目ポイントです。

環境

作ってみよう

package.json と nuxt.config.ts と tsconfig.json

nuxi コマンドは新しくできた nuxt の cli です。

package.json
{
  ...
  "scripts": {
    "dev": "nuxi dev",
    "build": "nuxi build",
    "start": "nuxi build && nuxi start",
    "generate": "nuxi generate"
  },
  "dependencies": {
    "@nuxt/content": "^2.9.0",
    "nuxt": "^3.8.1"
  }
}

nuxt をローカル実行すると .nuxt ディレクトリができるので、そこにできる .nuxt/tsconfig.json を extends します。

tsconfig.json
{
  "extends": "./.nuxt/tsconfig.json"
}

defineNuxtConfig で設定を定義します。head などは app の中になりました。

nuxt.config.ts
import { defineNuxtConfig } from "nuxt/config"

export default defineNuxtConfig({
  app: {
    head: {
      title: process.env.npm_package_name || '',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
      ],
      link: [
      ]
    },
  },
  modules: [
    "@nuxt/content",
  ]
})

ディレクトリ構造

これはほぼ前回のままですが、 staticpublic になりました。
また、_slug.vue[id].vue としています。動的なパスは [...] とするようになりました。

% tree . -N
.
├── content
│   └── news
│       ├── 20200526.md
│       └── 20200527.md
├── layouts
│   └── default.vue
├── node_modules
├── nuxt.config.ts
├── package-lock.json
├── package.json
├── tsconfig.json
├── pages
│   ├── index.vue
│   └── news
│       ├── [id].vue
│       └── index.vue
└── public
    └── images
        └── news
            ├── sample26.png
            └── sample27.png

一覧に表示: content ディレクトリから md を拾ってくる

queryContentcontent ディレクトリ配下のパスを取得します。
なお useAsyncData はキーでキャッシュを作るため、別の個所で limit(3) などでこの一覧を作成したいときは、 "news" と指定しているところを変える必要があります。

pages/news/index.vue
<template>
  <div>
    <div v-for="n in news" :key="n._path">
      <nuxt-link :to="n._path">{{n.title}} {{n.date}}</nuxt-link>
    </div>
  </div>
</template>

<script setup>
const { data: news } = await useAsyncData("news", () =>
  queryContent("news").limit(15).find()
)
useHead({
  title: "News",
})
</script>

詳細を表示: content ディレクトリから md を拾ってくる

useRoute()route.path で現在のパスと同じ content 配下のコンテンツを queryContent で拾いに行きます。
<ContentDoc>v-slot した中身にもろもろ反映させることができます。
useHead<title> や、指定すれば og:img なども反映させることができます。

pages/news/[id].vue
<template>
  <article>
    <ContentDoc v-slot="{ doc: news }">
      <h1>{{news.title}}</h1>
      <dl>
        <dt>date</dt>
        <dd>{{news.date}}</dd>
      </dl>
      <div><img :src="news.image" /></div>
      <div v-for="tag in news.tags" :key="tag">{{tag}}</div>
      <ContentRenderer :value="news" />
    </ContentDoc>
  </article>
</template>

<script setup>
const route = useRoute()
const news = await queryContent(route.path).findOne()
const meta = {
  title: news.title,
}
useHead(meta)
</script>

実際に実行してみて、起動できるかチェック

% npm run dev

http://localhost:3000 で確認できます。(terminal に URL 出てきます)

生成もしてみる

% npm run generate
% npx serve .output/public

動的なコードも .output/public に generate されます。
http://localhost:3000 で確認できます。(terminal に URL 出てきます)

上記サンプルソースコード

https://github.com/feb19/nuxt3-content-web

以前の Nuxt Content v1.0.0 だった頃のソースコードも残していますので見比べてみてくださいまし。

https://github.com/feb19/nuxt-content-web/

かんそう

Discussion