未分類
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)は:
- ファイルが
---で始まることを期待 - BOM があると
\ufeff---と認識 - YAML として認識できず、フロントマター全体を無視
publishedAtとtagsが抽出されずnullになる- 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" をデフォルトに
.editorconfigにcharset = 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 になる場合
- BOM を確認
head -n 1 content/file.md | od -c - YAML 構文を確認
# ✅ 正しい --- title: タイトル tags: - tag1 - tag2 --- # ❌ 間違い(インデントが不正) --- title: タイトル tags: - tag1 - tag2 --- - スキーマを確認
// content.config.ts publishedAt: z.coerce.date().optional() // ↑ coerce がないと文字列のままパースされる - SQLite の内容を確認
sqlite3 .data/content/contents.sqlite \ "SELECT id, publishedAt, tags FROM _content_pages;" - キャッシュをクリア
rm -rf .nuxt .data && pnpm dev
データベースが更新されない場合
- ファイル監視が動いているか確認
# ログを確認 pnpm dev --debug - ファイルパスが正しいか確認
// content.config.ts source: "**/*.{md,mdx}" // ← 拡張子を確認 - 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 でブラウザに通知 → リロード
重要なポイント
- ✅ SQLite は自動で管理される - 手動で SQL を書く必要なし
- ✅ 開発時はリアルタイム更新 - ファイル保存で即座に反映
- ✅ 本番は静的ダンプ - database.compressed.mjs として配信
- ⚠️ BOM に注意 - UTF-8 without BOM で保存する
- ⚠️ スキーマ変更時はキャッシュクリア -
.nuxtと.dataを削除