Back |Guide·

Nuxt Content 404 issues? One config can fix it

Enabled prerendering for optimization. Pushed to production. Suddenly receive weird 404 error. One config line fixes it - autoSubfolderIndex
NuxtBug FixPrerenderingCloudflare
Nuxt Content 404 issues? One config can fix it
This post is about prodction issue cause by prerendering.If you are here because you get many 404 with the development server, then that can be fixed by building after adding new content files.

Intro

Enabled prerendering today. I wanted to optimize the performance and the D1 db reads a bit.

Pushed to production and I was really lucky that someone reported a 404 error on my shared blog post on discord.

A quick review revealed the issue exist, so looking back my recent changes, the prerendering was a possible issue, so I wuickly reverted that change to fix the issue on prod.

But I was curious. Went to the blog index - worked fine. Clicked through to a post - worked fine. Everything seemed ok. So what broke for that direct link?

The Problem

After poking around, I found it. The file exists. The route is there. But direct URLs 404.

Blog index to post? Works. Direct link or refresh? 404.

Took way too long to spot it. That extra / at the end, just like an annoying comma in large codebase:

Navigate from index: /blog/my-post Hit the URL directly: /blog/my-post/

One character.

Why This Breaks

Prerendering creates /blog/my-post/index.html by default (subfolder with index file). When you hit that URL directly, browsers add a trailing slash. Standard browser behavior.

Your Nuxt Content query uses route.path:

queryCollection('posts').path(route.path).first()

Route path is now /blog/my-post/ (with slash). Content query looks for exact match. Finds nothing. Returns null. You get 404.

The dev experience fools you - clicking links works because client-side routing doesn't add the slash. Everything seems fine until someone refreshes or shares a direct link.

The Fix

One line in nuxt.config.ts:

nitro: {
  prerender: {
    autoSubfolderIndex: false,
    routes: ['/blog'],
    crawlLinks: true
  }
}

That's it. This tells Nuxt to create /blog/post.html instead of /blog/post/index.html. No subfolder, no index file, no trailing slash.

Refresh now? Works.

Why Not Just Handle It in Code?

You could normalize the path in your component:

const normalizedPath = computed(() => route.path.replace(/\/$/, ''))

This makes both URLs work. But now you have a worse problem: split analytics.

Umami (or GA, whatever you use) sees these as different pages:

  • /blog/my-post - 45 views
  • /blog/my-post/ - 23 views

Which one is the "real" number? Neither. You're splitting your traffic data for no reason.

Same with SEO. Google sees two URLs for the same content. Not ideal.

The config fix gives you one canonical URL. All traffic counts correctly. SEO stays clean. That's what you want.


The Workaround (If You Must)

Actually I fixed like this first, but I was not satisfied with this approach. Here's how to handle it in code:

<script setup lang="ts">
const route = useRoute()
const appConfig = useAppConfig()
const { locale } = useI18n()

// Normalize path by removing trailing slash
const normalizedPath = computed(() => route.path.replace(/\/$/, ''))

const { data: post } = await useAsyncData(
  `blog-post-${normalizedPath.value}-${locale.value}`,
  async () => {
    // Use normalized path for querying
    const path = normalizedPath.value
    const result = await queryCollection('posts')
      .path(path)
      .first()

    return result
  }
)
</script>

Both URLs work now. But analytics are still split. Not the solution I'd recommend.

Performance Tip

While you're here, add this to prevent client-side re-queries:

const { data: post } = await useAsyncData(
  `blog-post-${normalizedPath.value}`,
  async () => {
    // query logic
  },
  {
    getCachedData: (key) => useNuxtApp().payload.data[key]
  }
)

This tells Nuxt to use the prerendered data instead of querying again on the client. Small optimization, but it adds up.


Complete Example

Here's a working blog setup with the config fix:

pages/blog/[slug].vue
<script setup lang="ts">
const route = useRoute()

const { data: post } = await useAsyncData(
  `blog-post-${route.path}`,
  async () => {
    return await queryCollection('posts')
      .path(route.path)
      .first()
  },
  {
    getCachedData: (key) => useNuxtApp().payload.data[key]
  }
)

if (!post.value) {
  throw createError({ statusCode: 404, statusMessage: 'Post not found' })
}
</script>

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <ContentRenderer :value="post" />
  </div>
</template>

Why This Actually Matters

The real reason to use autoSubfolderIndex: false isn't just fixing the 404. It's your analytics.

Without the fix, Umami (or whatever you use) sees:

/blog/my-post       → 45 views
/blog/my-post/      → 23 views

Which number do you use? Neither's correct. You're splitting traffic across two URLs.

With the fix:

/blog/my-post       → 68 views

One canonical URL. Accurate numbers. SEO doesn't get confused by duplicate content. Everyone's happy.

Bottom Line

You would think AI will help these kind of issues easily, it just side tracked me this time, making the fix take longer than it should.

Prerendering is great for performance and reducing server load. But small config details can break things in unexpected ways. It was another reminder to always test the built app in preview mode before pushing to production. Catch these issues early.

With good setup, the fix can be deployed quickly and painlessly. Anyway, there are many things that break in the cloud - so the preview is not always enough, but it helps a lot.


Need Help with Your Nuxt App?

Running into prerendering issues or need help optimizing your Nuxt site? We can help you debug, optimize, and get your app deployed the right way.

Stay in the loop

Get the latest articles and updates delivered straight to your inbox.

Need a Landing Page?

Modern landing pages with optional modules (blog, docs, forms, i18n). Let's discuss your project.

Build Your MVP

Full-stack SaaS development. Expert in database design, multi-tenancy, and scalable architecture.

Deployment Help

Dockerize your backend, set up CI/CD pipelines, deploy to Cloudflare or Hetzner. Early-stage setup.

Suggest a SaaS Tool

Missing a calculator or tool? Suggest what you'd like to see on our site.