• #nuxt
  • #content
  • #troubleshooting
  • #frontmatter
  • #bom
未分類

フロントマター表示トラブルシューティング(2025-10-02)

発生した問題

DocPage コンポーネントで、タイトルの上に日付(publishedAt)とタグ(tags)を表示しようとしたが、以下の症状が発生:

  1. 日付とタグが表示されない
    • props.doc.publishedAtnull
    • props.doc.tagsnull
    • v-if 条件が false になり要素が非表示
  2. フロントマターが本文に表示される
    • Markdown のフロントマターがパースされず、本文内にテキストとして表示
    • 水平線(---)や YAML 構文がそのまま出力

原因の詳細分析

1. SQLite データベースの状態

このプロジェクトでは Nuxt Content が SQLite を使用している:

設定(nuxt.config.ts):

content: {
  documentDriven: true,
  database: {
    type: "sqlite"  // ← SQLite データベースを使用
  },
  experimental: {
    sqliteConnector: "native"  // ← Node.js 22.6+ のネイティブ SQLite
  }
}

データベースファイル:

  • 場所: .data/content/contents.sqlite
  • テーブル: _content_pages, _content_info, _development_cache

SQLite に保存されたデータの確認結果:

SELECT id, publishedAt, tags FROM _content_pages
WHERE id LIKE '%setup-guide%';

-- 結果:
-- id: pages/nuxt-content-mdx-setup-guide.md
-- publishedAt: 2025-10-01T00:00:00.000Z  ✅ 正しく保存されている
-- tags: ["nuxt","content","mdx","setup"]  ✅ 正しく保存されている

重要な発見:

  • SQLite データベースには正しくフロントマターが保存されていた
  • つまり、データベースの問題ではなく、Markdown ファイルのパース時の問題

2. 根本原因:BOM (Byte Order Mark)

BOM とは:

  • UTF-8 ファイルの先頭に付く特殊な文字 \ufeff (Zero Width No-Break Space)
  • Windows のテキストエディタ(特に古いエディタ)が自動で付加することがある
  • YAML パーサーはファイルが --- で始まることを期待するが、BOM があると \ufeff--- となり認識失敗

影響を受けたファイル:

# BOM が含まれていたファイル
content/nuxt-content-mdx-setup-guide.md    # ← \ufeff---
content/nuxt-content-mdx-requirements.md   # ← \ufeff---
content/index.mdx                          # ← \ufeff---

パース失敗の流れ:

  1. Markdown ファイルを読み込み
  2. BOM のせいで --- が認識されない → フロントマターとして扱われない
  3. フロントマターが本文の一部として扱われる
  4. publishedAttags が抽出されず null になる
  5. 本文内にフロントマターの内容がそのまま表示される

3. CSS の競合問題

日付要素の配置にも問題があった:

問題のあった構造:

<div class="doc__body">
  <p v-if="isoDate" class="doc__published">...</p>  <!-- ← body 内にあった -->
  <ContentRenderer :value="doc" />
</div>

問題のある CSS:

.doc__body :deep(div[data-content-id] > p:first-of-type) {
  display: none;  /* ← フロントマター削除用だが、日付も消してしまう */
}

このセレクタは本来「レンダリングされたフロントマターの段落を削除する」意図だったが、日付の段落も <p> 要素のため display: none が適用されてしまった。

解決方法

修正1: BOM の削除

実施内容:

# 正規表現で BOM を削除
# Before: \ufeff---
# After:  ---

修正したファイル:

  • content/nuxt-content-mdx-setup-guide.md
  • content/nuxt-content-mdx-requirements.md
  • content/index.mdx

結果:

  • フロントマターが正しくパースされるようになった
  • SQLite に保存された publishedAttags が Vue コンポーネントに正しく渡されるようになった

修正2: 日付要素の配置変更

変更前:

<article class="doc">
  <header v-if="doc.tags?.length" class="doc__header">...</header>
  <div class="doc__body">
    <p v-if="isoDate" class="doc__published">...</p>  <!-- body 内 -->
    <ContentRenderer :value="doc" />
  </div>
</article>

変更後:

<article class="doc">
  <header v-if="doc.tags?.length" class="doc__header">...</header>
  <p v-if="isoDate" class="doc__published">...</p>  <!-- body 外に移動 -->
  <div class="doc__body">
    <ContentRenderer :value="doc" />
  </div>
</article>

効果:

  • 日付要素が .doc__body :deep() セレクタの影響を受けなくなった
  • CSS の競合が解消された

Nuxt Content の SQLite 連携フロー

┌─────────────────────┐
│ Markdown ファイル    │
│ (content/*.md)      │
└──────────┬──────────┘
           │
           │ 1. ファイル読み込み
           │    ※ BOM があるとここで問題発生
           ↓
┌─────────────────────┐
│ Frontmatter Parser  │
│ (YAML パース)       │
└──────────┬──────────┘
           │
           │ 2. メタデータ抽出
           │    - title
           │    - publishedAt (Date 型に変換)
           │    - tags
           ↓
┌─────────────────────┐
│ SQLite Database     │
│ (.data/content/     │
│  contents.sqlite)   │
└──────────┬──────────┘
           │
           │ 3. クエリで取得
           │    queryCollection("pages")
           │      .path("/xxx")
           │      .first()
           ↓
┌─────────────────────┐
│ Vue Component       │
│ (props.doc)         │
│  - publishedAt ✅   │
│  - tags ✅          │
└─────────────────────┘

教訓とベストプラクティス

1. UTF-8 BOM に注意

BOM の確認方法:

# ファイルの先頭バイトを確認
head -n 1 file.md | od -c | head -n 1

# BOM がある場合: \357 \273 \277 が表示される

BOM を付けないエディタの設定:

  • VS Code: デフォルトで BOM なし UTF-8(推奨)
  • Notepad: "UTF-8 BOM なし" を選択
  • vim: set nobomb

新規ファイル作成時のチェックリスト:

  • エディタが "UTF-8 without BOM" で保存されている
  • フロントマターが --- で始まっている(先頭に空白や BOM がない)
  • YAML インデントが正しい(スペース2つ)

2. SQLite データベースのメンテナンス

キャッシュクリアが必要な状況:

  • フロントマターの形式を変更したとき
  • content.config.ts のスキーマを変更したとき
  • パース結果がおかしいとき

クリア方法:

# データベースと Nuxt キャッシュを削除
rm -rf .nuxt .data

# 開発サーバー再起動で自動再構築
pnpm dev

注意:

  • Windows では SQLite ファイルがロックされることがある
  • 開発サーバーを停止してから削除する必要がある

3. デバッグ手順

フロントマターが表示されない場合の調査順序:

  1. Vue コンポーネントで値を確認
    console.log('doc:', props.doc);
    console.log('publishedAt:', props.doc.publishedAt);
    console.log('tags:', props.doc.tags);
    
  2. SQLite データベースを確認
    sqlite3 .data/content/contents.sqlite \
      "SELECT publishedAt, tags FROM _content_pages WHERE id LIKE '%ファイル名%';"
    
  3. Markdown ファイルの BOM を確認
    head -n 1 content/file.md | od -c
    
  4. キャッシュをクリアして再起動
    rm -rf .nuxt .data && pnpm dev
    

4. CSS セレクタの設計

:deep() 使用時の注意点:

  • :deep() は子孫要素すべてに影響する
  • 意図しない要素にスタイルが適用される可能性がある
  • 特定の要素だけを対象にする場合、より具体的なセレクタを使う

良い例:

/* フロントマター由来の特定要素のみ削除 */
.doc__body :deep(div[data-content-id] > p:first-of-type),
.doc__body :deep(div[data-content-id] > p:first-of-type + ul),
.doc__body :deep(div[data-content-id] > p:first-of-type + ul + hr) {
  display: none;
}

悪い例:

/* すべての p 要素に影響してしまう */
.doc__body :deep(p) {
  display: none;
}

まとめ

今回のトラブルの原因:

  1. 主要因: Markdown ファイルの BOM により YAML フロントマターがパースされず、publishedAttagsnull になった
  2. 副次的要因: 日付要素が .doc__body 内にあり、CSS の :deep() セレクタで非表示になった

解決策:

  1. BOM を削除してフロントマターを正しくパース
  2. 日付要素を .doc__body の外に移動して CSS 競合を回避

SQLite の役割:

  • Nuxt Content は SQLite を使ってコンテンツをインデックス化
  • フロントマターのパースは SQLite 保存前に行われる
  • BOM があると YAML パースが失敗し、SQLite にも null で保存される
  • 今回は BOM 削除後、開発サーバー再起動で SQLite が自動的に再構築され、正しいデータが保存された

予防策:

  • UTF-8 BOM なしのエディタ設定を使用
  • フロントマターの書式を厳密に守る(--- で開始、インデント2スペース)
  • CSS セレクタは意図しない要素に影響しないよう具体的に記述