Nuxt Content 404 issues? One config can fix it
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:
<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?
Stay in the loop
Get the latest articles and updates delivered straight to your inbox.