ホーム 記事一覧 ブック DH週間トピックス 検索 このサイトについて
English
Nuxt Content + trailingSlash設定で静的ファイルへのリンクが404になる問題と解決策

Nuxt Content + trailingSlash設定で静的ファイルへのリンクが404になる問題と解決策

概要 Nuxt 3/4 + Nuxt Content の環境で trailingSlash: "append" を設定している場合、コンテンツ内のPDFや画像などの静的ファイルへのリンクが404エラーになることがあります。 発生条件 以下の条件がすべて揃った場合に発生します: Nuxt 3/4 + Nuxt Content を使用 nuxt.config.ts で trailingSlash: "append" を設定 Markdownやコンテンツ内に静的ファイル(PDF、画像など)へのリンクがある 問題の詳細 症状 コンテンツ内で以下のようなリンクを記述した場合: <a href="/uploads/document.pdf">資料をダウンロード</a> 生成されるHTMLでは、リンクが以下のように変換されてしまいます: /uploads/document.pdf/ 末尾に / が追加されるため、静的ファイルにアクセスできず404エラーになります。 原因 Nuxt Content では、Markdown内の <a> タグは ProseA コンポーネントに変換されます。 デフォルトの ProseA コンポーネント(@nuxtjs/mdc パッケージ内)は以下のような実装になっています: <template> <NuxtLink :href="props.href" :target="props.target"> <slot /> </NuxtLink> </template> NuxtLink を使用しているため、nuxt.config.ts の trailingSlash 設定の影響を受けます。 // nuxt.config.ts export default defineNuxtConfig({ experimental: { defaults: { nuxtLink: { trailingSlash: "append", // この設定が原因 }, }, }, }); 解決策 カスタムの ProseA コンポーネントを作成し、静的ファイルへのリンクの場合は通常の <a> タグを使用するようにします。 ...

Nuxt 4 SSGでローカルJSONファイルを正しく読み込む方法

Nuxt 4 SSGでローカルJSONファイルを正しく読み込む方法

はじめに Nuxt 4でStatic Site Generation (SSG) を行う際、ローカルのJSONファイルからデータを読み込んで静的ページを生成したいケースがあります。しかし、Next.jsのgetStaticPropsのようにシンプルにはいかず、いくつかのハマりポイントがあります。 本記事では、試行錯誤の末に見つけた正しいアプローチを紹介します。 問題:なぜ単純なfsの読み込みでは動かないのか 最初に試したアプローチ(失敗) // ❌ これは動かない const fetchLocalData = async (filePath: string) => { if (import.meta.server) { const fs = await import('fs'); const path = await import('path'); const fullPath = path.resolve(process.cwd(), 'public/data', filePath); const data = fs.readFileSync(fullPath, 'utf-8'); return JSON.parse(data); } // クライアントサイド const response = await fetch(`/data/${filePath}`); return await response.json(); }; このアプローチには以下の問題があります: process.cwd()がビルド環境で異なる: ローカル開発とVercel等のビルド環境では作業ディレクトリが異なる Nitroのプリレンダリング時にファイルが見つからない : SSG時、Nitroは独自のコンテキストで動作する useAsyncDataなしでは、クライアントサイドでも実行される : SSGの意味がなくなる 解決策:Nitro Storage API + Server APIルート + useAsyncData アーキテクチャ [SSGビルド時] Page Component ↓ useAsyncData Server API Route (/api/local-data/[...path]) ↓ useStorage (Nitro Storage API) public/data/*.json [生成されたHTML] _payload.json にデータが埋め込まれる → クライアントサイドでのJSON読み込み不要 Step 1: nuxt.config.tsでserverAssetsを設定 // nuxt.config.ts export default defineNuxtConfig({ nitro: { prerender: { crawlLinks: true, failOnError: false, }, // Nitro Storage APIでpublic/dataをサーバーアセットとしてマウント serverAssets: [{ baseName: 'data', dir: './public/data' }], }, routeRules: { '/**': { prerender: true }, }, }); Step 2: Server APIルートを作成(Nitro Storage + fsフォールバック) // server/api/local-data/[...path].ts import { readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; export default defineEventHandler(async (event) => { const pathParam = getRouterParam(event, 'path'); if (!pathParam) { throw createError({ statusCode: 400, message: 'Path is required' }); } const filePath = Array.isArray(pathParam) ? pathParam.join('/') : pathParam; // セキュリティ: パストラバーサル防止 if (filePath.includes('..')) { throw createError({ statusCode: 400, message: 'Invalid path' }); } // Nitro Storage APIを使用 // nuxt.config.tsのserverAssetsで定義した'data'ストレージにアクセス const storage = useStorage('assets:data'); // ファイルパスをストレージキーに変換(/を:に置換) const storageKey = filePath.replace(/\//g, ':'); // Storage APIで取得を試みる if (await storage.hasItem(storageKey)) { const data = await storage.getItem(storageKey); return data; } // フォールバック: 開発環境向けにfsで直接読み込み const fsPath = resolve(process.cwd(), 'public/data', filePath); if (existsSync(fsPath)) { const data = readFileSync(fsPath, 'utf-8'); return JSON.parse(data); } throw createError({ statusCode: 404, message: `File not found: ${filePath}` }); }); Nitro Storage APIを使うメリット: ...