• #nuxt
  • #content
  • #sqlite
  • #architecture
未分類

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

はじめに:なぜ SQLite なのか?

Nuxt Content v3 では、v2 のファイルベースストレージから SQLite ベースのストレージに移行しました。この変更により、以下のメリットが得られます:

  • I/O パフォーマンスの向上 - ファイルシステムへの逐次アクセスから SQL クエリベースの高速検索へ
  • バンドルサイズの削減 - 必要なデータのみを効率的にロード
  • エッジ環境のサポート - Cloudflare Workers などのサーバーレス環境での動作
  • オフラインアクセス - ブラウザ内で WASM SQLite を使ったローカルクエリ実行

公式ドキュメント: https://content.nuxt.com/blog/v3

SQLite データベースの構造

1. データベースファイルの場所

開発環境:

.data/content/contents.sqlite

ビルド時の生成物:

.nuxt/content/database.compressed.mjs  # 圧縮された SQLite ダンプ
.nuxt/content/sql_dump.txt             # SQL ダンプ(平文)

2. テーブル構造

Nuxt Content は以下のテーブルを自動生成します:

-- メタ情報テーブル
CREATE TABLE _content_info (
  id TEXT PRIMARY KEY,
  ready BOOLEAN,
  structureVersion VARCHAR,
  version VARCHAR,
  __hash__ TEXT UNIQUE
);

-- コレクションごとのテーブル(例:pages コレクション)
CREATE TABLE _content_pages (
  id TEXT PRIMARY KEY,
  title VARCHAR,
  body TEXT,
  description VARCHAR,
  extension VARCHAR,
  meta TEXT,
  navigation TEXT DEFAULT true,
  path VARCHAR,
  publishedAt DATE NULL,
  seo TEXT DEFAULT '{}',
  stem VARCHAR,
  tags TEXT NULL,
  __hash__ TEXT UNIQUE
);

重要なポイント:

  • content.config.ts で定義した各コレクションごとに、テーブルが作られる
  • フロントマターのフィールドは対応するカラムにマッピングされる
  • __hash__ は内容の整合性チェックに使用される

3. 実際のデータ例

INSERT INTO _content_pages VALUES (
  'pages/nuxt-content-mdx-setup-guide.md',     -- id
  'Nuxt Content + MDX サイトの初期構築手順',    -- title
  '{"type":"minimark","value":[...]}',         -- body (AST形式)
  'Nuxt 3 と Nuxt Content を使った...',       -- description
  'md',                                        -- extension
  '{}',                                        -- meta
  'true',                                      -- navigation
  '/nuxt-content-mdx-setup-guide',             -- path
  '2025-10-01T00:00:00.000Z',                 -- publishedAt ✅ ISO 8601形式
  '{"title":"...","description":"..."}',       -- seo
  'nuxt-content-mdx-setup-guide',              -- stem
  '["nuxt","content","mdx","setup"]',          -- tags ✅ JSON配列
  '0usg5x4l3ZsyIKgbbuRVgu_MonHd6WwhObeEIY3Iwvg' -- __hash__
);

Markdown から SQLite へのフロー

フェーズ 1: ビルド時のインデックス作成

┌─────────────────────────────┐
│ 1. Markdown ファイル読み込み  │
│    content/**/*.md           │
└──────────────┬──────────────┘
               │
               │ ① ファイル監視(開発時のみ)
               │    chokidar / Vite watch
               ↓
┌─────────────────────────────┐
│ 2. フロントマター解析         │
│    - YAML パーサー           │
│    - BOM チェック ⚠️         │
└──────────────┬──────────────┘
               │
               │ ② スキーマ検証
               │    content.config.ts
               ↓
┌─────────────────────────────┐
│ 3. Markdown → AST 変換      │
│    - remark/rehype          │
│    - MDX コンパイル          │
└──────────────┬──────────────┘
               │
               │ ③ AST を JSON に
               │    minimark 形式
               ↓
┌─────────────────────────────┐
│ 4. SQLite テーブル生成       │
│    - コレクション定義から     │
│    - CREATE TABLE IF NOT EXISTS │
└──────────────┬──────────────┘
               │
               │ ④ データ挿入
               │    INSERT INTO ...
               ↓
┌─────────────────────────────┐
│ 5. SQL ダンプ生成            │
│    .nuxt/content/sql_dump.txt │
│    database.compressed.mjs   │
└─────────────────────────────┘

フェーズ 2: ランタイムでのデータ取得

サーバーサイド(開発・本番):

// apps/web/app/pages/[...slug].vue
const { data: doc } = await useAsyncData(
  () => `content-${docPath.value}`,
  () => queryCollection("pages")
       .path(docPath.value)
       .first()
);

// ↓ 内部処理
// 1. .data/content/contents.sqlite を開く
// 2. SELECT * FROM _content_pages WHERE path = '/xxx'
// 3. 結果を ParsedContent 型で返す

クライアントサイド(ブラウザ):

1. 初回クエリ時に database.compressed.mjs をダウンロード
2. WASM SQLite でブラウザ内 DB を初期化
3. 以降のクエリはサーバーを介さずローカルで実行
   → ページ遷移が爆速 ⚡

ファイル監視と自動更新のメカニズム

開発環境での動作

Nuxt Content の HMR(Hot Module Replacement):

// 1. Vite の File Watcher が Markdown 変更を検知
//    → content/**/*.md

// 2. Nuxt Content のビルドプラグインが起動
//    @nuxt/content/dist/runtime/server/storage/builder

// 3. 変更ファイルを再パース
//    - フロントマター解析
//    - AST 生成
//    - __hash__ 計算

// 4. SQLite の該当レコードを UPDATE
UPDATE _content_pages
SET title = ?, body = ?, publishedAt = ?, __hash__ = ?
WHERE id = 'pages/xxx.md';

// 5. 整合性チェック
//    - _content_info.ready を false → true に更新

// 6. WebSocket で HMR イベント送信
//    → ブラウザがリロード

確認できるログ:

# .nuxt/content/sql_dump.txt の末尾
UPDATE _content_info SET ready = true WHERE id = 'checksum_pages';

本番環境での動作

本番ビルド時:

pnpm run build
# または
npx nuxi generate

# 1. すべての Markdown を一括パース
# 2. SQLite DB を完全構築
# 3. database.compressed.mjs を生成
# 4. 静的ファイルとして配信

重要: 本番環境では SQLite DB は読み取り専用。ファイル監視やリアルタイム更新は行われません。

BOM 問題の詳細

BOM とは

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

なぜ問題になるのか?

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

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

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

  1. ファイルが --- で始まることを期待
  2. BOM があると \ufeff--- と認識
  3. YAML として認識できず、フロントマター全体を無視
  4. publishedAttags が抽出されず null になる
  5. SQLite に 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

予防:

  • VS Code の設定で "UTF-8 without BOM" をデフォルトに
  • .editorconfigcharset = utf-8 を設定

データベース更新タイミングの詳細

開発時の自動更新

イベント処理SQLite 更新
Markdown 新規作成INSERT
Markdown 編集UPDATE
Markdown 削除DELETE
フロントマター変更UPDATE
content.config.ts 変更DROP & CREATE TABLE✅ (全再構築)
nuxt.config.ts 変更サーバー再起動✅ (全再構築)

キャッシュクリアが必要なケース

以下の場合は手動でキャッシュクリアが必要:

# SQLite DB が壊れた、パースエラーが直らない場合
rm -rf .nuxt .data
pnpm dev  # 自動で再構築される

注意(Windows):

  • 開発サーバーが SQLite ファイルをロック中
  • 削除前にサーバーを停止する必要あり
  • Ctrl+C でサーバー停止 → ファイル削除 → 再起動

queryCollection の内部動作

// ユーザーコード
const doc = await queryCollection("pages")
  .path("/nuxt-content-mdx-setup-guide")
  .first();

// ↓ 内部で実行される SQL
SELECT * FROM _content_pages
WHERE path = '/nuxt-content-mdx-setup-guide'
LIMIT 1;

// ↓ 結果をオブジェクトに変換
{
  id: "pages/nuxt-content-mdx-setup-guide.md",
  title: "Nuxt Content + MDX サイトの初期構築手順",
  publishedAt: new Date("2025-10-01T00:00:00.000Z"),
  tags: ["nuxt", "content", "mdx", "setup"],
  body: { type: "minimark", value: [...] },
  // ...
}

よく使うクエリパターン

// 1. パスで取得
queryCollection("pages").path("/xxx").first()
// → SELECT * FROM _content_pages WHERE path = '/xxx' LIMIT 1

// 2. すべて取得
queryCollection("pages").all()
// → SELECT * FROM _content_pages

// 3. タグでフィルタ
queryCollection("pages")
  .where({ tags: { $contains: "nuxt" } })
  .all()
// → SELECT * FROM _content_pages WHERE tags LIKE '%nuxt%'

// 4. 日付でソート
queryCollection("pages")
  .sort({ publishedAt: -1 })
  .limit(10)
  .all()
// → SELECT * FROM _content_pages
//    ORDER BY publishedAt DESC LIMIT 10

デプロイ時の考慮事項

Cloudflare Pages でのビルド

# wrangler.toml または GitHub Actions
build:
  command: pnpm run build
  # または
  command: npx nuxi generate

# 生成物:
# .output/public/
#   ├── _nuxt/
#   │   └── database.compressed.mjs  ← SQLite ダンプ
#   └── index.html

SQLite コネクタの選択

nuxt.config.ts:

export default defineNuxtConfig({
  content: {
    database: {
      type: "sqlite"
    },
    experimental: {
      sqliteConnector: "native"  // Node.js 22.5+ 推奨
      // または
      // sqliteConnector: "better-sqlite3"
      // sqliteConnector: "sqlite3"
    }
  }
})

推奨設定(2025年現在):

  • 開発環境: native (Node.js 22.6+)
  • 本番環境: 静的生成なので影響なし

トラブルシューティングチェックリスト

フロントマターが null になる場合

  1. BOM を確認
    head -n 1 content/file.md | od -c
    
  2. YAML 構文を確認
    # ✅ 正しい
    ---
    title: タイトル
    tags:
      - tag1
      - tag2
    ---
    
    # ❌ 間違い(インデントが不正)
    ---
    title: タイトル
    tags:
    - tag1
    - tag2
    ---
    
  3. スキーマを確認
    // content.config.ts
    publishedAt: z.coerce.date().optional()
    // ↑ coerce がないと文字列のままパースされる
    
  4. SQLite の内容を確認
    sqlite3 .data/content/contents.sqlite \
      "SELECT id, publishedAt, tags FROM _content_pages;"
    
  5. キャッシュをクリア
    rm -rf .nuxt .data && pnpm dev
    

データベースが更新されない場合

  1. ファイル監視が動いているか確認
    # ログを確認
    pnpm dev --debug
    
  2. ファイルパスが正しいか確認
    // content.config.ts
    source: "**/*.{md,mdx}"  // ← 拡張子を確認
    
  3. Vite キャッシュをクリア
    rm -rf node_modules/.vite
    

まとめ

Nuxt Content の SQLite フロー(要約)

1. Markdown ファイル作成/編集
   ↓
2. Vite/Chokidar がファイル変更を検知
   ↓
3. Nuxt Content ビルドプラグインが起動
   ↓
4. フロントマター → YAML パース (⚠️ BOM に注意)
   ↓
5. Markdown → AST → JSON (minimark)
   ↓
6. content.config.ts のスキーマで検証
   ↓
7. SQLite テーブルに INSERT/UPDATE
   ↓
8. __hash__ で整合性チェック
   ↓
9. .nuxt/content/sql_dump.txt を生成
   ↓
10. HMR でブラウザに通知 → リロード

重要なポイント

  1. SQLite は自動で管理される - 手動で SQL を書く必要なし
  2. 開発時はリアルタイム更新 - ファイル保存で即座に反映
  3. 本番は静的ダンプ - database.compressed.mjs として配信
  4. ⚠️ BOM に注意 - UTF-8 without BOM で保存する
  5. ⚠️ スキーマ変更時はキャッシュクリア - .nuxt.data を削除

参考リンク