Nuxt Content v2を利用したMarkdownによるブログ記事の作成方法

2023-09-25
2023-09-25

前回の記事の続きです。今回はNuxt Content v2を利用してMarkdownで記事を書く際に気になった点について書きました。

前回の記事:Nuxt 3、Tailwind、Nuxt content v2によるブログ作成のための基本設定

基本的な記事の書き方

markdownを直接書くことです。

一般的なmarkdownと違って悩むところとしては内部リンクと画像の挿入時のパスの書き方です。

まず、内部リンクは以下のようにプロジェクト直下のcontentフォルダをルートとした相対パスで記述します。

content直下の場合:[test1](<file名>)
content配下のtestフォルダ内のページの場合:[test2](test/<file名>)

上記からも分かるようにルーティングに関してはcontent直下がルートになります。かなり簡単にブログのパスを切れるので、私のように既存のURLを変えたくない場合には重宝します。

参考:Content Directory

続いて画像の挿入については以下のようにプロジェクト直下のpublicフォルダをルートとした相対パスで書きます。

public直下の場合:![test1](<file名>)
public配下のtestフォルダ内のページの場合:![test2](test/<file名>)

フロントマターの設定

ブログ内でさまざまなデザインや設定を実現するためにmarkdown記事にフロントマターを設定し、コードから記事情報を利用することができます。

以下はシンプルな例です。

---
title: 'ページタイトル'
description: 'ページの詳細説明'
---
記事本文

フロントマターのネイティブパラメータは以下の通りです。

キータイプデフォルト説明
titlestringページの最初の<h1>ページのタイトル。メタタグにも挿入されます
descriptionstringページの最初の<p>ページの説明。タイトルの下に表示され、メタタグに挿入されます
draftboolfalseページを下書きとしてマークし、開発モードでのみ表示します。
navigationbooltrueページnavigation の戻り値に含まれるかどうかを定義します。
headobjecttrueuseContentHead で簡単にアクセスできます。

(参考:markdown

これ以外にも任意でフロントマターにキーを設定し、コードから参照することが可能です。

このブログでは追加で作成日時と更新日時、タグ、サムネイルのパスを設定しています。

---
title: Nuxt content v2を使用した記事の書き方
createdAt: '2023-09-25'
updatedAt: '2023-09-25'
tags: ['Nuxt','Nuxt Content v2']
draft: false
description:  
thumbnail: '/img/twitter-card.png'
---

コンポーネントの利用

Nuxt Content v2ではMDC (Markdown Components) と呼ばれるMarkdownからコンポーネントを呼び出す記法が可能です。これにより、通常のmarkdownでは難しかった複雑なデザインを作ることができます。

以下はよくあるインフォボックスのコンポーネントを作成し、markdownから使ってみた例です。

なおコンポーネントのデザインにはTailwindを使用しています。こちらの設定については前回記事で紹介しています。

markdownファイルでは以下のように書きます。

# MDC test
::info
test
::

呼び出し元のコンポーネントは以下のように書きます。ファイル名はInfo.vueです。注意点としてなぜか<p>タグを使うと[Vue warn]: Hydration children mismatch...というエラーが出たので使わない方が良さそうです。

<template>
    <div class="p-4 rounded-md bg-blue-100 border border-blue-200">
        <div class="flex">
            <div class="flex-shrink-0">
                <svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
                    fill="currentColor" aria-hidden="true">
                    <path fill-rule="evenodd"
                        d="M10 18a8 8 0 100-16 8 8 0 000 16zM9 9a1 1 0 011-1h1a1 1 0 110 2H9a1 1 0 01-1-1zm0 4a1 1 0 011-1h1a1 1 0 110 2H9a1 1 0 01-1-1z"
                        clip-rule="evenodd" />
                </svg>
            </div>
            <div class="ml-3 text-sm text-blue-700">
                <slot />
            </div>
        </div>
    </div>
</template>

上記は一番簡単なMDCのパターンです。ちょっと複雑なものとしてmarkdown側から引数を受け取ることも可能です。

コンポーネント側は以下のように書きます。

<script setup lang="ts">
interface Props {
    name: string
}
const props = defineProps<Props>();
</script>
<template>
    <div class=" text-red-500 text-5xl">
        {{ props.name }}
    </div>
</template>

markdown側では以下のように波括弧内に引数を書きます。

# MDC test
::info
test
::
::test{name=fuga}
::

結果

mdc

注意点としてmarkdownで使いたいコンポーネントは通常のcomponentsフォルダに置くのではなく、components/content配下に置く必要があります。また、開発中に コンポーネントを追加・変更後は改めてyarn devなどを実行しないと反映されません。

新記事のテンプレート生成

上記で紹介したように、markdownで記事を書く際にフロントマターを細かく書かなければなりません。

毎回コピペと日付入力作業が面倒だったので、以前のTailwind Nextjs Starter Blogが使っていた新記事生成スクリプトを改造して使ってみたので紹介します。

スクリプトでは、手動入力が必要な項目がターミナルから入力を求められます。自分の場合はtitle,description,tagsは手動入力するようにしています。それ以外の作成日時などの項目は自動で入力されます。

こちらがコードです。

// TypeScriptで使う場合、型定義をインポートまたは定義する
import inquirer from 'inquirer'
import dedent from 'dedent'
import path from 'path'
import fs from 'fs'
const genFrontMatter = (answers) => {
  let d = new Date()
  const date = [
    d.getFullYear(),
    ('0' + (d.getMonth() + 1)).slice(-2),
    ('0' + d.getDate()).slice(-2),
  ].join('-')
  const tagArray = answers.tags.split(',')
  tagArray.forEach((tag, index) => (tagArray[index] = tag.trim()))
  const tags = "'" + tagArray.join("','") + "'"
  let frontMatter = dedent`---
  title: ${answers.title ? answers.title : 'Untitled'}
  createdAt: '${date}'
  updatedAt: '${date}'
  tags: [${answers.tags ? tags : ''}]
  draft: ${answers.draft === 'yes' ? true : false}
  description: ${answers.description ? answers.description : ' '}
  thumbnail: '/img/twitter-card.png'
  `
  frontMatter = frontMatter + '\n---'
  return frontMatter
}
inquirer
  .prompt([
    {
      name: 'title',
      message: 'Enter post title:',
      type: 'input',
    },
    {
      name: 'description',
      message: 'Enter post description:',
      type: 'input',
    },
    {
      name: 'draft',
      message: 'Set post as draft?',
      type: 'list',
      choices: ['yes', 'no'],
    },
    {
      name: 'tags',
      message: 'Any Tags? Separate them with , or leave empty if no tags.',
      type: 'input',
    },
  ])
  .then((answers) => {
    // Remove special characters and replace space with -
    const fileName = answers.title
      .toLowerCase()
      .replace(/[^a-zA-Z0-9 ]/g, '')
      .replace(/ /g, '-')
      .replace(/-+/g, '-')
    const frontMatter = genFrontMatter(answers)
    const filePath = `content/contents/${fileName ? fileName : 'untitled'}.md`
    fs.writeFile(filePath, frontMatter, { flag: 'wx' }, (err) => {
      if (err) {
        throw err
      } else {
        console.log(`Blog post generated successfully at ${filePath}`)
      }
    })
  })
  .catch((error) => {
    if (error.isTtyError) {
      console.log("Prompt couldn't be rendered in the current environment")
    } else {
      console.log('Something went wrong, sorry!')
    }
  })

このコードをscripts配下において新記事作成を行う際はnode ./scripts/compose.jsで実行しています。

実行するとターミナルでtitleやtagsなどを入力でき、実行後はそれらがフロントマターに反映された状態でmarkdownファイルが生成されます。

まとめ

今回は前回の環境構築系の話に続く第二弾として実際の記事の書き方などについて紹介しました。フロントマターやコンポーネントを駆使して適度にカスタマイズできるのがNuxt Contentのいいところですね。

次回は一般的なブログでよく使われるページの作り方を紹介しようと思います。