• #nuxt
  • #nuxt-content
  • #architecture
  • #sqlite
開発nuxt-content-docsメモ

Nuxt Pages と Nuxt Content の統合仕組み

このドキュメントでは、Nuxt の pages ディレクトリと Nuxt Content の content ディレクトリの違い、SQLite の役割、記事一覧への統合方法を包括的に解説します。

📚 目次

  1. Nuxt と Nuxt Content の基本
  2. SQLite の役割
  3. pages ディレクトリ vs content ディレクトリ
  4. 記事一覧への統合の仕組み
  5. 実装の詳細

基本概念

Nuxt のルーティング

Nuxt 自体の機能(Nuxt Content とは別):

  • app/pages/ ディレクトリにファイルを配置すると、自動的にルート(URL)が生成される
  • これは Nuxt の基本機能で、SQLite とは無関係
  • 例:
    • app/pages/index.vue/ (トップページ)
    • app/pages/about.vue/about
    • app/pages/flowchart/customer-type-identification.vue/flowchart/customer-type-identification

Nuxt Content の役割

Nuxt Content は別のモジュール:

  • content/ ディレクトリに Markdown/MDX ファイルを配置
  • Nuxt Content が自動的に SQLite データベースに変換・保存
  • これにより高速な記事検索・取得が可能に

SQLite の役割

Nuxt Content と SQLite の関係

content/                        SQLite Database
├── article1.md      ─────→    ┌─────────────────┐
├── article2.mdx     ─────→    │  pages テーブル │
└── guide.md         ─────→    │                 │
                                │  - title        │
                                │  - path         │
                                │  - publishedAt  │
                                │  - tags         │
                                │  - content      │
                                └─────────────────┘

重要なポイント:

  1. SQLite は Nuxt Content 専用
    • content/ ディレクトリのファイルのみが SQLite に保存される
    • pages/ ディレクトリの .vue ファイルは SQLite に保存されない
  2. 自動変換プロセス
    • Nuxt Content が起動時に content/ を監視
    • Markdown/MDX を解析してメタデータと本文を抽出
    • SQLite の pages コレクション(テーブル)に保存
    • これはNuxt Content の仕様
  3. 開発環境 vs 本番環境
    • 開発: Node.js 22.6 の node:sqlite モジュールを使用
    • 本番 (Cloudflare Pages): ネイティブ SQLite を使用

なぜ Nuxt Content は SQLite を使うのか?

1. 高速な検索・クエリ処理

Markdown ファイルを毎回読み込んで解析するのは非常に遅いです:

❌ ファイルシステムアプローチ(遅い)
リクエスト → ファイル読み込み → Markdown解析 → HTML変換 → レスポンス
              (100ms)        (50ms)      (30ms)     = 180ms/記事

✅ SQLite アプローチ(速い)
リクエスト → SQLiteクエリ → レスポンス
              (5ms)         = 5ms/記事

2. 複雑な検索が可能

SQLite なら SQL で柔軟にクエリできます:

// タグで検索
queryCollection('pages')
  .where('tags', 'contains', 'nuxt')
  .all()

// 日付範囲で検索
queryCollection('pages')
  .where('publishedAt', '>', '2025-01-01')
  .sort('publishedAt', 'desc')
  .all()

// 全文検索
queryCollection('pages')
  .where('content', 'contains', 'SQLite')
  .all()

ファイルシステムだと、これらの検索はすべてのファイルを読み込んでJavaScript で処理する必要があります(遅い)。

3. ビルド時の事前処理

開発時・ビルド時:
content/article.md → Markdown解析 → SQLiteに保存(1回だけ)

実行時:
リクエスト → SQLiteから取得(解析不要、超高速)

Markdown の解析は重い処理なので、1回だけ実行して結果を保存しておきます。

4. メタデータの一括取得

記事一覧を表示する場合:

// ❌ ファイルシステムなら
// 100個の記事 × 各ファイル読み込み = 遅い
const files = await readdir('content')
const articles = await Promise.all(
  files.map(async file => {
    const content = await readFile(file)
    const parsed = await parseMarkdown(content) // 遅い
    return parsed.frontmatter
  })
)

// ✅ SQLite なら
// 1回のクエリで全メタデータ取得 = 速い
const articles = await queryCollection('pages')
  .only(['title', 'publishedAt', 'tags'])
  .all()

5. Cloudflare Pages などのエッジ環境での最適化

Cloudflare Pages や他のエッジ環境では:

  • ファイルシステムへのアクセスが制限される場合がある
  • SQLite なら単一ファイルで完結
  • エッジランタイムでも高速動作

パフォーマンス比較

SQLite を使わない場合(毎回 Markdown を読む):

100記事のサイトで記事一覧表示
→ 100ファイル読み込み + 100回の解析 = 数秒かかる ❌

SQLite を使う場合(事前に変換済み):

100記事のサイトで記事一覧表示
→ 1回のSQLクエリ = 数ミリ秒 ✅

まとめ: SQLite を使う理由

理由詳細
🚀 パフォーマンスMarkdown の毎回の解析を避ける
🔍 検索機能SQL で複雑なクエリが可能
事前処理ビルド時に1回だけ変換
📊 一括取得メタデータを効率的に取得
🌐 エッジ対応エッジ環境での最適化

結論: SQLite は Nuxt Content のパフォーマンス最適化のための仕組みです。毎回 Markdown ファイルを読んで解析するより、事前に SQLite に変換しておく方が圧倒的に速いからです。


ディレクトリの違い

content/ ディレクトリ(Nuxt Content)

特徴詳細
ファイル形式.md, .mdx
ルーティングNuxt Content が自動生成
データベース✅ SQLite に自動保存される
取得方法queryCollection('pages').all()
一覧への追加自動 - SQLite から取得するだけ
用途ブログ記事、ドキュメント、通常のコンテンツ

例:

content/
├── article1.md          → /article1 (Nuxt Content が生成)
└── guide/setup.md       → /guide/setup (Nuxt Content が生成)

app/pages/ ディレクトリ(Nuxt 標準)

特徴詳細
ファイル形式.vue
ルーティングNuxt が自動生成(ファイルシステムルーティング)
データベース❌ SQLite には保存されない
取得方法router.getRoutes() でルート情報を取得
一覧への追加手動 - メタ情報を定義して統合が必要
用途カスタム Vue コンポーネント、複雑な UI、外部スクリプト使用ページ

例:

app/pages/
├── index.vue                               → / (Nuxt が生成)
├── about.vue                               → /about (Nuxt が生成)
└── flowchart/customer-type-identification.vue  → /flowchart/customer-type-identification (Nuxt が生成)

統合の仕組み

現在の実装(app/pages/index.vue

<script setup>
import { queryCollection } from "#imports"

// 1. Nuxt Content から SQLite 経由で記事を取得
const { data: contentArticles } = await useAsyncData('all-articles', () =>
  queryCollection('pages').all()  // ← SQLite から全記事を取得
)

// 2. Nuxt Router から pages ディレクトリの情報を取得
const router = useRouter()
const customPages = computed(() => {
  const routes = router.getRoutes()  // ← Nuxt のルート情報を取得
  return routes
    .filter(route => route.meta?.includeInList === true)  // ← 条件でフィルタ
    .map(route => ({
      path: route.path,
      title: route.meta?.title,
      publishedAt: route.meta?.publishedAt,
      tags: route.meta?.tags
    }))
})

// 3. 両方を結合して一覧表示
const articles = computed(() => {
  const content = contentArticles.value || []
  return [...content, ...customPages.value]  // ← 統合
})
</script>

データフロー図

┌─────────────────────────────────────────────────────────────┐
│                      記事一覧の統合                          │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────┐          ┌─────────────────────────┐
│  content/           │          │  app/pages/             │
│  ├── article1.md    │          │  └── flowchart/         │
│  ├── article2.mdx   │          │      └── custom.vue     │
│  └── guide.md       │          │                         │
└──────────┬──────────┘          └────────────┬────────────┘
           │                                  │
           │ Nuxt Content が                 │ Nuxt が
           │ 自動変換                         │ ルート生成
           ↓                                  ↓
┌─────────────────────┐          ┌─────────────────────────┐
│  SQLite Database    │          │  Router (メモリ上)      │
│  pages テーブル      │          │  ルート情報              │
└──────────┬──────────┘          └────────────┬────────────┘
           │                                  │
           │ queryCollection()                │ router.getRoutes()
           │ で取得                           │ で取得
           ↓                                  ↓
┌─────────────────────┐          ┌─────────────────────────┐
│  contentArticles    │          │  customPages            │
│  (SQLite の記事)     │          │  (meta.includeInList    │
│                     │          │   が true のページ)      │
└──────────┬──────────┘          └────────────┬────────────┘
           │                                  │
           └──────────────┬───────────────────┘
                          ↓
                 ┌─────────────────┐
                 │   articles      │
                 │  (統合された一覧) │
                 └─────────────────┘
                          ↓
                    テーブル表示

実装の詳細

1. Nuxt Content の記事(SQLite から取得)

取得コード:

const { data: contentArticles } = await useAsyncData('all-articles', () =>
  queryCollection('pages').all()
)

特徴:

  • content/ 配下のすべての Markdown/MDX が自動的に SQLite に保存される
  • queryCollection('pages') で SQLite から取得
  • メタデータ(title, publishedAt, tags など)も自動抽出
  • 追加作業不要 - ファイルを置くだけで一覧に表示

2. カスタム Vue ページ(Router から取得)

取得コード:

const router = useRouter()
const customPages = computed(() => {
  const routes = router.getRoutes()
  return routes
    .filter(route => route.meta?.includeInList === true)
    .map(route => ({
      path: route.path,
      title: route.meta?.title || route.name || route.path,
      publishedAt: route.meta?.publishedAt || new Date().toISOString().split('T')[0],
      tags: route.meta?.tags || []
    }))
})

特徴:

  • router.getRoutes() で Nuxt のすべてのルート情報を取得
  • SQLite とは無関係 - メモリ上のルート情報のみ
  • definePageMeta({ includeInList: true }) でフィルタ
  • 手動でメタ情報を定義する必要がある

3. 統合処理

統合コード:

const articles = computed(() => {
  const content = contentArticles.value || []
  return [...content, ...customPages.value]
})

特徴:

  • JavaScript の配列として単純に結合
  • SQLite への追加はしない - あくまで表示用の統合
  • ソート・ページネーションは結合後に実施

よくある質問

Q1: pages ディレクトリの Vue ファイルは SQLite に保存されますか?

A: いいえ、保存されません。

  • SQLite に保存されるのは content/ ディレクトリの Markdown/MDX のみ
  • pages/.vue ファイルは Nuxt のルーティングシステムでのみ管理
  • 記事一覧には router.getRoutes() で取得した情報を一時的に結合しているだけ

Q2: 記事一覧に表示するために SQLite に追加する処理を実装していますか?

A: いいえ、SQLite への追加処理は実装していません

  • Nuxt Content の記事: SQLite から取得(自動)
  • カスタム Vue ページ: Router から取得(手動メタ情報)
  • 両方をメモリ上で結合して表示しているだけ
  • データベースへの書き込みは一切なし

Q3: includeInList: true の設定はどこに保存されますか?

A: Vue ファイルのメタ情報としてコード内に保存されます。

<script setup>
definePageMeta({
  includeInList: true,  // ← これは Vue ファイル内のメタ情報
  title: 'ページタイトル'
})
</script>
  • Nuxt がビルド時にこの情報をルートメタデータに組み込む
  • 実行時に router.getRoutes() で取得可能
  • SQLite には保存されない - あくまでルート情報の一部

Q4: この仕組みは Nuxt の仕様ですか、Nuxt Content の仕様ですか?

A: 両方の仕様を組み合わせています。

機能仕様
pages/ ディレクトリのルーティングNuxt の仕様
content/ → SQLite 自動変換Nuxt Content の仕様
router.getRoutes()Nuxt の仕様
queryCollection()Nuxt Content の仕様
記事一覧への統合処理当プロジェクトの独自実装

まとめ

データソースの整理

データソース取得方法SQLite一覧への追加
content/ の Markdown/MDXqueryCollection('pages').all()✅ 自動保存される自動(SQLite から取得)
pages/ の Vue ファイルrouter.getRoutes()❌ 保存されない手動(メタ情報定義)

重要なポイント

  1. SQLite は Nuxt Content 専用
    • content/ のファイルのみが自動的に SQLite に保存される
    • これは Nuxt Content の仕様
  2. pages ディレクトリは Nuxt の標準機能
    • ファイルシステムベースのルーティング
    • SQLite とは無関係
    • ルート情報はメモリ上で管理
  3. 記事一覧の統合は独自実装
    • SQLite から Nuxt Content の記事を取得
    • Router から pages の情報を取得
    • メモリ上で結合して表示
    • SQLite への追加処理は存在しない
  4. includeInList: true の役割
    • Vue ファイルのメタ情報として定義
    • Router のルート情報に含まれる
    • フィルタ条件として使用
    • SQLite には保存されない

この仕組みにより、Nuxt Content の記事(SQLite ベース)とカスタム Vue ページ(Router ベース)を柔軟に統合した記事一覧システムが実現できています。