Overview

In a Nuxt 3/4 + Nuxt Content environment with trailingSlash: "append" configured, links to static files such as PDFs and images within content may result in 404 errors.

Conditions for Occurrence

This occurs when all of the following conditions are met:

  1. Using Nuxt 3/4 + Nuxt Content
  2. trailingSlash: "append" is set in nuxt.config.ts
  3. There are links to static files (PDFs, images, etc.) in Markdown or content

Problem Details

Symptoms

When writing a link like the following in content:

<a href="/uploads/document.pdf">Download document</a>

The generated HTML converts the link to:

/uploads/document.pdf/

A / is appended to the end, making the static file inaccessible and resulting in a 404 error.

Cause

In Nuxt Content, <a> tags in Markdown are converted to ProseA components.

The default ProseA component (in the @nuxtjs/mdc package) has the following implementation:

<template>
  <NuxtLink :href="props.href" :target="props.target">
    <slot />
  </NuxtLink>
</template>

Since it uses NuxtLink, it is affected by the trailingSlash setting in nuxt.config.ts.

// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    defaults: {
      nuxtLink: {
        trailingSlash: "append", // This setting is the cause
      },
    },
  },
});

Solution

Create a custom ProseA component that uses a regular <a> tag for links to static files.

Implementation

Create 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;
  // For external links or those already containing baseURL, use as-is
  if (href.startsWith("http") || href.startsWith(baseURL)) {
    return href;
  }
  // Add baseURL to internal relative paths (starting with /)
  if (href.startsWith("/")) {
    return baseURL + href;
  }
  return href;
});
</script>

Key Points

  1. Static file detection: Determines whether a file is static based on the file extension
  2. Use regular <a> tag: For static files, use a regular <a> tag instead of NuxtLink to avoid the trailingSlash setting
  3. Add baseURL: When not using NuxtLink, baseURL is not automatically applied, so it must be added manually

Verification

After building, check the generated HTML:

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

Expected output (no trailing /):

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

Additional Notes

Why Use trailingSlash: “append”

  • From an SEO perspective, it is recommended to unify the presence or absence of trailing / in URLs
  • Prevents /page and /page/ from being treated as different URLs (duplicate content issue)
  • Many sites adopt append (adding trailing /)

Other Solutions

  1. Use absolute URLs: Write absolute URLs like https://example.com/uploads/document.pdf in content (though management becomes cumbersome if URLs differ per environment)

  2. Remove trailingSlash setting: Change the site-wide setting (need to consider impact on other pages)

Environment

  • Nuxt 4.x (the same issue may occur with Nuxt 3.x)
  • Nuxt Content v3
  • @nuxtjs/mdc

References