• #nuxt
  • #content
  • #sqlite
  • #database
  • #mapping
  • #frontmatter
開発nuxt-content-docsメモ

Nuxt Content: SQLite フィールドマッピング仕様

概要

Nuxt Content v3では、マークダウンファイルを解析し、各要素をSQLiteのフィールドに自動マッピングします。このドキュメントでは、どの情報がどのフィールドに格納されるかを詳しく説明します。

SQLiteテーブル構造

_content_pages テーブル(例)

CREATE TABLE _content_pages (
  id TEXT PRIMARY KEY,           -- ファイルパス(コレクション名/ファイル名)
  title VARCHAR,                 -- タイトル(H1 または frontmatter)
  body TEXT,                     -- 本文のAST(JSON形式)
  description VARCHAR,           -- 説明文(frontmatter)
  extension VARCHAR,             -- 拡張子(md, mdx など)
  meta TEXT,                     -- メタ情報(JSON)
  navigation TEXT DEFAULT true,  -- ナビゲーション表示フラグ
  path VARCHAR,                  -- URLパス
  publishedAt DATE NULL,         -- 公開日(frontmatter)
  seo TEXT DEFAULT '{}',         -- SEO情報(JSON)
  stem VARCHAR,                  -- ファイル名(拡張子なし)
  tags TEXT NULL,                -- タグ(JSON配列)
  __hash__ TEXT UNIQUE           -- ハッシュ値(整合性チェック用)
);

フィールドマッピングルール

1. id(主キー)

ルール: コレクション名/ファイルパス

ファイル: content/nuxt-setup.md
↓
id: "pages/nuxt-setup.md"

ファイル: content/blog/2025-10-04/article.md
↓
id: "pages/blog/2025-10-04/article.md"

特徴:

  • ファイルパスがそのままIDになる
  • コレクション名(デフォルト: pages)が接頭辞として付く
  • 一意性が保証される

2. title(タイトル)

優先順位:

  1. フロントマターの title(最優先)
  2. 本文の最初の # 見出し(H1)
  3. ファイル名(最後の手段)

例:

---
title: "Nuxt Content セットアップ"  ← これが優先
---

# 実際の見出し  ← frontmatter がなければこれ

本文...

↓ SQLiteに格納

title: "Nuxt Content セットアップ"

フロントマターなしの場合:

# Nuxt Content セットアップ  ← これがタイトルになる

本文...

↓ SQLiteに格納

title: "Nuxt Content セットアップ"

3. body(本文)

ルール: マークダウン本文を AST(Abstract Syntax Tree)形式 でJSON化

元のマークダウン:

# タイトル

これは**太字**です。

- リスト1
- リスト2

SQLiteに格納されるJSON(minimark形式):

{
  "type": "minimark",
  "value": [
    {
      "type": "heading",
      "depth": 1,
      "value": [{ "type": "text", "value": "タイトル" }]
    },
    {
      "type": "paragraph",
      "value": [
        { "type": "text", "value": "これは" },
        { "type": "strong", "value": [{ "type": "text", "value": "太字" }] },
        { "type": "text", "value": "です。" }
      ]
    },
    {
      "type": "list",
      "ordered": false,
      "value": [
        { "type": "listItem", "value": [{ "type": "text", "value": "リスト1" }] },
        { "type": "listItem", "value": [{ "type": "text", "value": "リスト2" }] }
      ]
    }
  ],
  "toc": {
    "title": "",
    "searchDepth": 2,
    "depth": 2,
    "links": []
  }
}

特徴:

  • マークダウンをそのまま保存するのではなく、構造化されたASTを保存
  • レンダリング時にASTからHTMLを生成
  • 目次(TOC)情報も含まれる

4. description(説明文)

ルール: フロントマターの description フィールドのみ

---
description: "Nuxt Contentのセットアップ方法を解説します"
---

# タイトル
本文...

↓ SQLiteに格納

description: "Nuxt Contentのセットアップ方法を解説します"

注意:

  • フロントマターにない場合は "" (空文字列)
  • 本文から自動抽出されることはない

5. extension(拡張子)

ルール: ファイルの拡張子(. なし)

ファイル: content/article.md
↓
extension: "md"

ファイル: content/component.mdx
↓
extension: "mdx"

6. meta(メタ情報)

ルール: フロントマターのカスタムフィールドが格納される(JSON形式)

---
title: "記事タイトル"
author: "田中太郎"
category: "技術"
draft: true
customField: "カスタム値"
---

↓ SQLiteに格納(titleなど標準フィールド以外)

{
  "author": "田中太郎",
  "category": "技術",
  "draft": true,
  "customField": "カスタム値"
}

特徴:

  • title, description, publishedAt, tags などは専用フィールドに格納
  • それ以外のフィールドは meta にJSON形式で格納
  • アクセス方法: article.meta.author

7. navigation(ナビゲーション表示フラグ)

ルール: フロントマターの navigation フィールド(デフォルト: true

---
navigation: false  ← ナビゲーションに表示しない
---

↓ SQLiteに格納

navigation: "false"

デフォルト値:

---
# navigation フィールドがない場合
---

↓ SQLiteに格納

navigation: "true"

8. path(URLパス)

ルール: ファイルパスから生成されるURL

ファイル: content/nuxt-setup.md
↓
path: "/nuxt-setup"

ファイル: content/blog/2025-10-04/article.md
↓
path: "/blog/2025-10-04/article"

ファイル: content/docs/getting-started.md
↓
path: "/docs/getting-started"

特徴:

  • 拡張子(.md, .mdx)は削除される
  • content/ は削除される
  • 先頭に / が付く

9. publishedAt(公開日)

ルール: フロントマターの publishedAt または date フィールド

---
publishedAt: 2025-10-04
---

↓ SQLiteに格納(ISO 8601形式)

publishedAt: "2025-10-04T00:00:00.000Z"

注意:

  • content.config.tsz.coerce.date() を指定すると日付型に変換
  • 指定なしの場合は文字列のまま
  • フロントマターにない場合は NULL

10. seo(SEO情報)

ルール: 自動生成される SEO メタデータ(JSON形式)

---
title: "記事タイトル"
description: "記事の説明"
---

↓ SQLiteに格納

{
  "title": "記事タイトル",
  "description": "記事の説明"
}

デフォルト値(フロントマターなし):

{
  "title": "ファイル名",
  "description": ""
}

11. stem(ファイル名)

ルール: ファイル名(拡張子なし)

ファイル: content/nuxt-setup.md
↓
stem: "nuxt-setup"

ファイル: content/blog/article.mdx
↓
stem: "article"

12. tags(タグ)

ルール: フロントマターの tags フィールド(JSON配列形式)

---
tags: [nuxt, content, mdx]
highlight: "feature"
---

↓ SQLiteに格納

["nuxt", "content", "mdx"]

別の書き方(YAML形式):

---
tags:
highlight: "feature"
  - nuxt
  - content
  - mdx
---

↓ SQLiteに格納(同じ結果)

["nuxt", "content", "mdx"]

注意:

  • フロントマターにない場合は NULL
  • 空配列 []NULL は区別される

13. hash(ハッシュ値)

ルール: ファイル内容のハッシュ値(SHA-256ベース)

ファイル内容が変わると __hash__ も変わる
↓
キャッシュ無効化や整合性チェックに使用

用途:

  • コンテンツの変更検知
  • キャッシュ無効化
  • データベースの整合性チェック

フロントマター解析の優先順位

1. 標準フィールド(専用カラムに格納)

---
title: "タイトル"              # → title カラム
description: "説明"            # → description カラム
publishedAt: 2025-10-04       # → publishedAt カラム
tags: [tag1, tag2]            # → tags カラム
highlight: "feature"
navigation: false             # → navigation カラム
---

2. カスタムフィールド(meta カラムに JSON 格納)

---
author: "著者名"               # → meta.author
category: "カテゴリ"           # → meta.category
draft: true                   # → meta.draft
customField: "カスタム値"      # → meta.customField
---

実際のデータ例

マークダウンファイル

ファイル: content/nuxt-content-sqlite-architecture.md

---
title: "Nuxt Content の SQLite アーキテクチャ解説"
description: "Nuxt Content v3 の内部構造を解説"
publishedAt: 2025-10-02
tags: [nuxt, content, sqlite, architecture]
highlight: "feature"
author: "開発者"
category: "技術解説"
---

# Nuxt Content の SQLite アーキテクチャ解説

## はじめに

Nuxt Content v3 では...

SQLiteレコード

{
  "id": "pages/nuxt-content-sqlite-architecture.md",
  "title": "Nuxt Content の SQLite アーキテクチャ解説",
  "body": {
    "type": "minimark",
    "value": [
      {
        "type": "heading",
        "depth": 1,
        "value": [{ "type": "text", "value": "Nuxt Content の SQLite アーキテクチャ解説" }]
      },
      {
        "type": "heading",
        "depth": 2,
        "value": [{ "type": "text", "value": "はじめに" }]
      },
      {
        "type": "paragraph",
        "value": [{ "type": "text", "value": "Nuxt Content v3 では..." }]
      }
    ],
    "toc": {
      "title": "",
      "searchDepth": 2,
      "depth": 2,
      "links": [
        { "id": "はじめに", "depth": 2, "text": "はじめに" }
      ]
    }
  },
  "description": "Nuxt Content v3 の内部構造を解説",
  "extension": "md",
  "meta": {
    "author": "開発者",
    "category": "技術解説"
  },
  "navigation": true,
  "path": "/nuxt-content-sqlite-architecture",
  "publishedAt": "2025-10-02T00:00:00.000Z",
  "seo": {
    "title": "Nuxt Content の SQLite アーキテクチャ解説",
    "description": "Nuxt Content v3 の内部構造を解説"
  },
  "stem": "nuxt-content-sqlite-architecture",
  "tags": ["nuxt", "content", "sqlite", "architecture"],
  "__hash__": "L4S9l2rcBELA6hSUjDWpPK1rdi6NAtetbVcFo2s0GbM"
}

content.config.ts でのスキーマ定義

スキーマ定義の例

import { defineContentConfig, defineCollection, z } from "@nuxt/content"

export default defineContentConfig({
  collections: {
    pages: defineCollection({
      type: "page",
      source: "**/*.{md,mdx}",
      schema: z.object({
        // 必須フィールド
        title: z.string(),

        // オプションフィールド(型変換あり)
        publishedAt: z.coerce.date().optional(),
        updatedAt: z.coerce.date().optional(),

        // オプションフィールド(配列)
        tags: z.array(z.string()).optional(),

        // オプションフィールド(文字列)
        description: z.string().optional(),
        author: z.string().optional(),
        category: z.string().optional(),

        // オプションフィールド(真偽値)
        draft: z.boolean().optional(),
        navigation: z.boolean().default(true),
      })
    })
  }
})

スキーマ定義の効果

  1. 型チェック
    // ビルド時にフロントマターの型をチェック
    publishedAt: "invalid-date"  // ← エラー
    tags: "string"               // ← エラー(配列が必要)
    
  2. 型変換(coerce)
    publishedAt: z.coerce.date()
    
    // フロントマター
    publishedAt: "2025-10-04"
    
    // ↓ 自動で Date 型に変換
    publishedAt: new Date("2025-10-04T00:00:00.000Z")
    
  3. デフォルト値
    navigation: z.boolean().default(true)
    
    // フロントマターになくても true が設定される
    

BOM(Byte Order Mark)問題

BOM とは

  • UTF-8ファイルの先頭に付く 0xEF 0xBB 0xBF(文字では \ufeff
  • Windowsのメモ帳などが自動で付加することがある

問題

# BOM あり(パース失敗)
---
title: タイトル
---

# BOM なし(パース成功)
---
title: タイトル
---

YAMLパーサー(js-yaml)は:

  • ファイルが --- で始まることを期待
  • BOMがあると \ufeff--- と認識
  • YAMLとして認識できず、フロントマター全体を無視
  • publishedAttags が抽出されず null になる

検出方法

# BOMをチェック
head -n 1 content/file.md | od -c | head -n 1
# BOMがあれば: 357 273 277 が表示される

削除方法

# VS Code: 右下の "UTF-8 with BOM" → "UTF-8" に変更

# または正規表現で削除
sed -i '1s/^\xEF\xBB\xBF//' content/file.md

予防方法

  1. VS Codeの設定
    {
      "files.encoding": "utf8",
      "files.autoGuessEncoding": false
    }
    
  2. .editorconfig
    [*.{md,mdx}]
    charset = utf-8
    

フィールドアクセス方法

Vue コンポーネント内

<script setup>
const { data: article } = await useAsyncData('article', () =>
  queryCollection('pages').path('/nuxt-setup').first()
)
</script>

<template>
  <div>
    <!-- 標準フィールド -->
    <h1>{{ article.title }}</h1>
    <p>{{ article.description }}</p>
    <time>{{ article.publishedAt }}</time>

    <!-- タグ(配列) -->
    <div>
      <span v-for="tag in article.tags" :key="tag">
        {{ tag }}
      </span>
    </div>

    <!-- カスタムフィールド(meta) -->
    <p>著者: {{ article.meta.author }}</p>
    <p>カテゴリ: {{ article.meta.category }}</p>

    <!-- 本文レンダリング -->
    <ContentRenderer :value="article" />
  </div>
</template>

TypeScript 型定義

interface ParsedContent {
  id: string
  title: string
  body: {
    type: 'minimark'
    value: any[]
    toc: {
      title: string
      searchDepth: number
      depth: number
      links: Array<{
        id: string
        depth: number
        text: string
      }>
    }
  }
  description: string
  extension: string
  meta: Record<string, any>
  navigation: boolean
  path: string
  publishedAt: Date | null
  seo: {
    title: string
    description: string
  }
  stem: string
  tags: string[] | null
  __hash__: string
}

よくある質問

Q1: フロントマターに書いたフィールドが meta に入らない

原因: 標準フィールドは専用カラムに格納される

---
title: "タイトル"        # → title カラム(meta には入らない)
publishedAt: 2025-10-04  # → publishedAt カラム(meta には入らない)
author: "著者"           # → meta.author(カスタムフィールド)
---

Q2: 本文の最初の見出しがタイトルにならない

原因: フロントマターの title が優先される

---
title: "フロントマターのタイトル"  ← これが優先
---

# 本文の見出し  ← 無視される

解決策: フロントマターの title を削除

---
# title フィールドを削除
---

# 本文の見出し  ← これがタイトルになる

Q3: タグが null になる

原因1: BOMが付いている

# BOMをチェック
head -n 1 content/file.md | od -c

原因2: YAML構文が間違っている

# ✅ 正しい
tags: [tag1, tag2]
highlight: "feature"
# または
tags:
highlight: "feature"
  - tag1
  - tag2

# ❌ 間違い
tags: tag1, tag2
highlight: "feature"

原因3: スキーマ定義がない

// content.config.ts
tags: z.array(z.string()).optional()
highlight: "feature"

まとめ

フィールドマッピングの優先順位

  1. id: ファイルパス(自動生成)
  2. title: フロントマター → H1 → ファイル名
  3. body: マークダウン本文(AST形式)
  4. description: フロントマターのみ
  5. publishedAt: フロントマター(date または publishedAt)
  6. tags: フロントマター(配列形式)
  7. meta: その他のカスタムフィールド

重要なポイント

  • ✅ フロントマターの標準フィールドは専用カラムに格納
  • ✅ カスタムフィールドは meta に JSON 格納
  • ✅ 本文はAST形式でJSON化される
  • ⚠️ BOMがあるとフロントマター全体が無視される
  • ⚠️ title はフロントマターが最優先

参考リンク