開発nuxt-content-docsメモ
Nuxt Contentでのマークダウン画像管理
概要
このドキュメントでは、Nuxt Contentのマークダウンファイルで画像を扱う方法を説明します。以下の3つの環境で正しく動作します。
- VS Codeのマークダウンプレビュー
- ローカル開発サーバー(
pnpm dev) - Cloudflare Pagesへのデプロイ
ディレクトリ構成
apps/web/
├── content/
│ └── 2025-10-17/
│ ├── article.md
│ └── image.png
├── server/
│ └── middleware/
│ └── content-images.ts
└── nuxt.config.ts
マークダウンの記法
マークダウンファイルでは相対パスを使用します:

画像はマークダウンファイルと同じディレクトリに配置してください。これで3つの環境すべてで動作します。
実装の詳細
1. VS Codeプレビュー
VS Codeのマークダウンプレビューは、同じディレクトリからの相対パスを自動的に解決します。
2. ローカル開発(pnpm dev)
ファイル: apps/web/server/middleware/content-images.ts
import { defineEventHandler, setResponseHeaders, sendStream } from 'h3'
import { createReadStream, existsSync, statSync } from 'node:fs'
import { join } from 'node:path'
export default defineEventHandler(async (event) => {
// リクエストURLを取得
const url = event.node.req.url || ''
// 日付ディレクトリ内の画像ファイルにマッチ(例:/2025-10-17/image.png)
const match = url.match(/^\/(\d{4}-\d{2}-\d{2})\/([^/]+\.(png|jpg|jpeg|gif|webp|svg))$/i)
if (!match) return
// マッチした情報を取得(日付ディレクトリ、ファイル名、拡張子)
const [, dateDir, filename, ext] = match
// contentディレクトリ内のファイルパスを構築
const contentPath = join(process.cwd(), 'content', dateDir, filename)
// ファイルが存在しない場合は処理を中断
if (!existsSync(contentPath)) return
// ファイルでない場合(ディレクトリなど)は処理を中断
if (!statSync(contentPath).isFile()) return
// 拡張子とMIMEタイプのマッピング
const mimeTypes: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
svg: 'image/svg+xml'
}
// レスポンスヘッダーを設定(Content-Typeとキャッシュ制御)
setResponseHeaders(event, {
'Content-Type': mimeTypes[ext.toLowerCase()] || 'application/octet-stream',
'Cache-Control': 'public, max-age=31536000'
})
// ファイルをストリームとして返す
return sendStream(event, createReadStream(contentPath))
})
このミドルウェアは画像リクエストをインターセプトし、contentディレクトリから直接配信します。
3. Cloudflare Pagesへのデプロイ
ファイル: apps/web/nuxt.config.ts
以下のインポートを追加:
import { resolve } from "node:path";
import { copyFile, mkdir, readdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
Nuxt configに以下のフックを追加:
export default defineNuxtConfig({
// ... other config
hooks: {
// Nitroのビルド時、publicアセットが生成された後に実行されるフック
'nitro:build:public-assets': async (nitro) => {
// コピー元:contentディレクトリ
const contentDir = resolve(__dirname, 'content');
// コピー先:.output/publicディレクトリ
const publicDir = resolve(nitro.options.output.publicDir);
// ディレクトリを再帰的に走査して画像をコピーする関数
async function copyImages(dir: string, relativePath = '') {
// ディレクトリ内のファイル・フォルダ一覧を取得
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = join(dir, entry.name);
const currentRelativePath = join(relativePath, entry.name);
if (entry.isDirectory()) {
// ディレクトリの場合は再帰的に処理
await copyImages(sourcePath, currentRelativePath);
} else if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(entry.name)) {
// 画像ファイルの場合はコピー
const targetPath = join(publicDir, currentRelativePath);
const targetDir = join(targetPath, '..');
// コピー先ディレクトリが存在しない場合は作成
if (!existsSync(targetDir)) {
await mkdir(targetDir, { recursive: true });
}
// ファイルをコピー
await copyFile(sourcePath, targetPath);
console.log(`Copied image: ${currentRelativePath}`);
}
}
}
// contentディレクトリから画像のコピーを開始
await copyImages(contentDir);
console.log('✓ Content images copied to public directory');
}
}
})
このフックはビルドプロセス中に実行され、contentから.output/publicにすべての画像をコピーし、ディレクトリ構造を維持します。
4. 画像のスタイリング
ファイル: apps/web/app/components/DocPage.vue
画像がコンテンツ幅に収まるようCSSを追加:
.doc__body :deep(img) {
max-width: 100%;
height: auto;
display: block;
margin: 1.5rem 0;
}
ワークフロー
content/YYYY-MM-DD/にマークダウンファイルを作成- VS Codeに画像を貼り付け - 同じディレクトリに自動保存される
- 相対パスで画像を参照:
 - VS Codeプレビューですぐに画像が表示される
pnpm devを実行 - server middlewareで画像が表示されるpnpm run buildを実行 - 画像がpublicディレクトリにコピーされる- Cloudflare Pagesにデプロイ -
publicディレクトリから画像が配信される
メリット
- 単一の真実の源:画像は
contentディレクトリに配置 - 手動コピー不要:ビルドプロセスがデプロイを処理
- どこでも動作:VS Code、ローカル開発、本番環境
- シンプルなマークダウン:標準的な相対パスを使用
- パス変更不要:すべての環境で同じ記法が動作
デプロイコマンド
# プロジェクトをビルド
pnpm run build
# Cloudflare Pagesにデプロイ
pnpm exec wrangler pages deploy apps/web/.output/public --project-name=mdx-playground-web