🙆

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

に公開

概要

Nuxt 3/4 + Nuxt Content の環境で trailingSlash: "append" を設定している場合、コンテンツ内のPDFや画像などの静的ファイルへのリンクが404エラーになることがあります。

発生条件

以下の条件がすべて揃った場合に発生します:

  1. Nuxt 3/4 + Nuxt Content を使用
  2. nuxt.config.tstrailingSlash: "append" を設定
  3. 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.tstrailingSlash 設定の影響を受けます。

// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    defaults: {
      nuxtLink: {
        trailingSlash: "append", // この設定が原因
      },
    },
  },
});

解決策

カスタムの ProseA コンポーネントを作成し、静的ファイルへのリンクの場合は通常の <a> タグを使用するようにします。

実装

components/content/ProseA.vue を作成:

<template>
  <a v-if="isStaticFile" :href="resolvedHref" :target="props.target">
    <slot />
  </a>
  <NuxtLink v-else :href="props.href" :target="props.target">
    <slot />
  </NuxtLink>
</template>

<script setup lang="ts">
const props = defineProps({
  href: {
    type: String,
    default: "",
  },
  target: {
    type: String,
    default: undefined,
    required: false,
  },
});

const config = useRuntimeConfig();
const baseURL = config.app.baseURL || "";

const staticFileExtensions = [
  ".pdf",
  ".jpg",
  ".jpeg",
  ".png",
  ".gif",
  ".webp",
  ".svg",
  ".zip",
  ".doc",
  ".docx",
  ".xls",
  ".xlsx",
  ".ppt",
  ".pptx",
  ".csv",
  ".txt",
];

const isStaticFile = computed(() => {
  const href = props.href.toLowerCase();
  return staticFileExtensions.some((ext) => href.endsWith(ext));
});

const resolvedHref = computed(() => {
  const href = props.href;
  // 外部リンクまたは既にbaseURLが含まれている場合はそのまま
  if (href.startsWith("http") || href.startsWith(baseURL)) {
    return href;
  }
  // 内部の相対パス(/で始まる)にbaseURLを追加
  if (href.startsWith("/")) {
    return baseURL + href;
  }
  return href;
});
</script>

ポイント

  1. 静的ファイルの判定: ファイル拡張子で静的ファイルかどうかを判定
  2. 通常の <a> タグを使用: 静的ファイルの場合は NuxtLink ではなく通常の <a> タグを使用することで、trailingSlash 設定の影響を回避
  3. baseURLの追加: NuxtLink を使わない場合、baseURLが自動適用されないため、手動で追加

動作確認

ビルド後、生成されたHTMLを確認:

npm run generate
grep -o 'href="[^"]*\.pdf[^"]*"' .output/public/path/to/page/index.html

期待する出力(末尾に / がない):

href="/base-url/uploads/document.pdf"

補足

なぜ trailingSlash: "append" を使うのか

  • SEOの観点から、URLの末尾の / の有無を統一することが推奨される
  • /page/page/ が別のURLとして扱われる(重複コンテンツ問題)のを防ぐ
  • 多くのサイトでは append(末尾に / を追加)を採用

他の解決策

  1. 絶対URLを使用: コンテンツ内で https://example.com/uploads/document.pdf のように絶対URLを記述する(ただし、環境ごとにURLが異なる場合は管理が煩雑)

  2. trailingSlash設定を削除: サイト全体の設定を変更する(他のページへの影響を考慮する必要あり)

環境

  • Nuxt 4.x(Nuxt 3.xでも同様の問題が発生する可能性あり)
  • Nuxt Content v3
  • @nuxtjs/mdc

参考

Discussion