Fixing Nuxt Content Static Site Trailing Slash 404 Issue on Refresh

3 min
AI 总结
$
|
Article Summary Image
Article Summary Image

Problem Description

When building a static blog with Nuxt Content, I encountered a strange issue:

  • Navigating from the homepage to article links works perfectly
  • But when refreshing on an article page, there’s a momentary 404, then the content loads
  • The URL automatically changes from /2025/macai to /2025/macai/ (adding a trailing slash)

This issue only occurs in production environments (Netlify, Cloudflare Pages, GitHub Pages), not in local development.

Root Cause

1. Nuxt’s Pre-rendering Mechanism

When Nuxt executes nuxt generate for static site generation (SSG), it defaults to converting routes into subfolder structures:

/2025/macai  →  /2025/macai/index.html
/about       →  /about/index.html

This structure causes browsers to automatically add / at the end of URLs, because they’re actually accessing index.html inside a directory.

2. Nuxt Content’s Exact Path Matching

In [...slug].vue, we query articles using Nuxt Content:

const { data: post } = await useAsyncData(
  route.path,
  () => queryCollection('content').path(route.path).first(),
)

The problem is:

  • The article path in the Content database is /2025/macai (no trailing slash)
  • But on refresh, route.path is /2025/macai/ (with trailing slash)
  • Exact match fails → returns null → shows 404

3. Why Does Homepage Navigation Work?

Navigation from the homepage uses Vue Router client-side routing, which doesn’t trigger real HTTP requests, so there’s no trailing slash issue.

Page refresh uses server routing (even on static hosting), loading the actual HTML file, which triggers the trailing slash addition.

Solution

The core solution is to change Nuxt’s static file generation strategy so it no longer generates subfolder structures.

Configure Nitro’s autoSubfolderIndex option in nuxt.config.ts:

// Import environment variable detection tools, like std-env or use process.env directly
// import { isCI } from 'std-env'

export default defineNuxtConfig({
  nitro: {
    prerender: {
      // Disable subfolder index in CI/CD environment (production build)
      autoSubfolderIndex: process.env.CI ? false : undefined,
    },
  },
})

Configuration Effect:

  • Generates /2025/macai.html instead of /2025/macai/index.html
  • Browser loads the corresponding .html file directly when accessing /2025/macai
  • URL won’t automatically add trailing slash
  • Root path / still generates index.html (Nuxt handles root directory automatically)

Why Only Enable in CI Environment?

  • Local development environment (npm run dev) uses dev server, doesn’t need this config, keeping default behavior avoids unnecessary interference.
  • This issue mainly exists on static hosting platforms (Netlify/Cloudflare Pages/GitHub Pages), so it only needs to take effect during build deployment.

Technical Details

How autoSubfolderIndex Works

Config ValueGenerated StructureAccess URLDescription
true (default)/path/index.html/path/Traditional subfolder structure, causes trailing slash
false/path.html/pathFlat structure, perfectly solves the problem
undefinedAuto-detect based on environment-Default behavior in dev environment

Why Not Use router.options.strict?

Initially tried using:

router: {
  options: {
    strict: true,
  },
},

But this makes Vue Router strictly distinguish between /path and /path/, actually worsening the problem, because pre-rendering still generates subfolder structure by default, causing URLs with slashes to fail route matching.

Why Not Use Redirect Rules?

Nitro’s routeRules doesn’t support dynamic function redirects:

// ❌ Not supported
routeRules: {
  '/**': {
    redirect: {
      to: (path) => path.endsWith('/') ? path.slice(0, -1) : path
    }
  },
}

Only static redirect rules can be written, unable to handle all dynamic article paths.

Impact Scope

This configuration affects:

  • ✅ All article routes (/2025/macai)
  • ✅ All page routes (/about, /archive)
  • ✅ Dynamic routes ([...slug].vue)
  • ✅ Root path / still remains index.html

Won’t affect:

  • ❌ API routes (/api/*)
  • ❌ Special files (/atom.xml, /favicon.ico)
  • ❌ Existing redirect rules

Verification Method

Local Testing

# Generate static site
pnpm generate

# Preview generated site (note: preview server behavior may differ slightly from real environment)
pnpm preview

# Most accurate method is to check generated file structure
ls -la dist/2025/
# Should see macai.html instead of macai/index.html

Production Environment Testing

  1. Deploy to Netlify/Cloudflare Pages/GitHub Pages
  2. Visit article page (e.g., https://example.com/2025/macai)
  3. Refresh page, check:
    • Does URL remain /2025/macai (no trailing slash)
    • Is there no 404 flash
    • Does content load normally

Summary

The essence of this problem is the inconsistency between Nuxt pre-rendering’s file structure and Nuxt Content’s path matching mechanism.

The most elegant solution isn’t patching in frontend code (manually removing slashes), but changing the static file generation structure from the source by configuring autoSubfolderIndex: false. This not only solves the 404 issue but also makes the site’s URL structure more standardized and unified.