---
title: "404 Pages and SEO in Nuxt"
description: "Return missing-content HTTP errors in Nuxt and diagnose detected soft 404s that Google excludes from search."
canonical_url: "https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/404-pages"
last_updated: "2026-10-05"
---

> Index of every page on this site: [llms.txt](https://nuxtseo.com/llms.txt)

::key-takeaways
- Legitimate missing URLs can return 404. Google can detect error-like content with a successful response as a soft 404 and exclude it
- Nuxt's `error.vue` and `createError()`{lang="ts"} support server error responses; static hosting needs its own correct 404 behavior
- Google treats 404 and 410 as the same signal for indexing, so pick whichever your app returns naturally rather than engineering one specifically
::

Legitimate missing URLs can return 404: deleted products and user typos are common examples. Repair broken internal links to pages readers still need. Google removes indexed URLs that consistently return these statuses.

A detected soft 404 serves error-like or empty content despite a successful HTTP response. Google excludes detected soft 404s from search results. Diagnose the response and content together.

During SSR, use Nuxt error handling and check the response status. For static output, configure and test the host's missing-route response. An error component alone does not guarantee the HTTP status.

## Quick Setup

For Nuxt 4's default directory structure, create `app/error.vue`:

```vue [app/error.vue]
<script setup lang="ts">
const props = defineProps({
  error: Object
})

useHead({
  title: props.error?.statusCode === 404 ? '404 - Page Not Found' : 'Error',
  meta: [
    { name: 'robots', content: 'noindex' }
  ]
})
</script>

<template>
  <div class="error-page">
    <template v-if="error?.statusCode === 404">
      <h1>Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
      <NuxtLink to="/">
        Go home
      </NuxtLink>
    </template>
    <template v-else>
      <h1>An error occurred</h1>
      <p>{{ error?.message }}</p>
    </template>
  </div>
</template>
```

This initial-load excerpt requires an app-owned product endpoint returning a product or an HTTP error. Handle a successful empty result as missing data:

```vue [pages/products/[id\\].vue]
<script setup lang="ts">
const route = useRoute()
const { data: product, error } = await useFetch(`/api/products/${route.params.id}`)

if (error.value)
  throw error.value
if (!product.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Product not found'
  })
}

useSeoMeta({
  title: product.value.name,
  description: product.value.description
})
</script>
```

Throwing the error during SSR can produce a 404 response. For static generation, test the deployed host’s missing-route behavior separately.

## Soft 404 Errors Explained

Soft 404 detection happens when Google sees content that looks like an error page but receives `200 OK` status ([Google Search Central](https://developers.google.com/search/docs/crawling-indexing/http-network-errors)).

Common triggers:

- "Page not found" in title or heading
- An empty or error-like page served with a successful status
- Redirecting all 404s to homepage
- Empty page body with "coming soon" message
- Generic error messages without meaningful content

Google Search Console flags soft 404s in the "Page Indexing" report. Fix by returning proper `404` status code.

A soft 404 can exclude a page you intended to publish. If content moved, use a relevant permanent redirect. If it disappeared without a replacement, return 404 or 410.

## Checking Routes and Throwing Errors

This initial-load Content excerpt requires a configured `blog` collection with title and description fields. Collection failures propagate; an absent document produces 404:

```vue [pages/blog/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
const { data: post, error } = await useAsyncData(`post-${route.params.slug}`, () =>
  queryCollection('blog').path(`/blog/${route.params.slug}`).first())

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

useSeoMeta({
  title: post.value.title,
  description: post.value.description
})
</script>

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

Throwing `createError()`{lang="ts"} during server-side page setup invokes Nuxt error handling. Check the resulting status. `error.vue` displays the error; it does not originate it.

## Dynamic Routes Considerations

This initial-load excerpt requires an app-owned product endpoint supplying name, description, and image. Check upstream failures before absent data:

```vue [pages/products/[id\\].vue]
<script setup lang="ts">
const route = useRoute()

const { data: product, error } = await useFetch(`/api/products/${route.params.id}`)

if (error.value)
  throw error.value
if (!product.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Product not found'
  })
}

// Product exists, continue with meta tags
useSeoMeta({
  title: product.value.name,
  description: product.value.description,
  ogImage: product.value.image
})
</script>

<template>
  <div class="product">
    <h1>{{ product.name }}</h1>
    <p>{{ product.description }}</p>
  </div>
</template>
```

Your server endpoint must distinguish missing data from infrastructure failures. Throw an HTTP 404 for an absent product. Propagate upstream failures instead of returning an error-shaped object as product data.

## 404 vs 410 Status Codes

**404 Not Found**: resource doesn't exist, might never have existed, might come back:

- User typos
- Outdated external links
- Deleted products that might return to inventory
- Seasonal content (holiday pages)

**410 Gone**: resource existed, now permanently removed:

- Discontinued products
- Deleted blog posts (no redirect target)
- Expired promotions
- Intentionally removed content

Google [treats them the same for indexing](https://developers.google.com/search/docs/crawling-indexing/http-network-errors): both statuses tell the crawler the content doesn't exist, and previously indexed URLs get dropped from search results either way. Use 410 only when you want to state explicitly that the content is gone for good; 404 is fine for everything else.

To return 410:

```vue [pages/discontinued/[id\\].vue]
<script setup lang="ts">
throw createError({
  statusCode: 410,
  statusMessage: 'This product has been discontinued'
})
</script>
```

## Custom 404 Page Design

Good 404 pages keep users on your site:

```vue [app/error.vue]
<script setup lang="ts">
const props = defineProps({
  error: Object
})

const is404 = computed(() => props.error?.statusCode === 404)

useHead({
  title: is404.value ? '404 - Page Not Found' : 'Error',
  meta: [
    { name: 'robots', content: 'noindex' }
  ]
})
</script>

<template>
  <div class="not-found">
    <template v-if="is404">
      <h1>Page Not Found</h1>
      <p>The page you're looking for doesn't exist or has moved.</p>

      <SearchBox />

      <nav>
        <h2>Popular Pages:</h2>
        <ul>
          <li>
            <NuxtLink to="/products">
              Products
            </NuxtLink>
          </li>
          <li>
            <NuxtLink to="/blog">
              Blog
            </NuxtLink>
          </li>
          <li>
            <NuxtLink to="/support">
              Support
            </NuxtLink>
          </li>
        </ul>
      </nav>

      <NuxtLink to="/">
        Go to Homepage
      </NuxtLink>
    </template>
    <template v-else>
      <h1>An error occurred</h1>
      <p>{{ error?.message }}</p>
      <NuxtLink to="/">
        Go to Homepage
      </NuxtLink>
    </template>
  </div>
</template>
```

**Don't:**

- Redirect all 404s to homepage (soft 404 risk)
- Auto-redirect after countdown (bad UX)
- Show only "404" with no explanation
- Display technical error messages

**Do:**

- Explain what happened clearly
- Provide search functionality
- Link to popular/relevant pages
- Match site design (keeps users oriented)
- Include contact option for reporting broken links

## Crawl Budget Impact

A real 404 costs you little: Google's [crawling frequency for a URL gradually decreases](https://developers.google.com/search/docs/crawling-indexing/http-network-errors) once it keeps returning 404, and a newly discovered 404 never enters the indexing pipeline at all. Soft 404 detection considers both the successful status and error-like content. Inspect the cause instead of inferring a crawl schedule.

For maintained sites:

- Monitor 404 rates in Search Console
- Fix internal links pointing to 404s
- Remove 404 URLs from [sitemap](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers/sitemaps)
- Use 301 redirects for high-value deleted pages with relevant replacements

Don't worry about occasional 404s from external links or user typos.

## Handling 404s for Deleted Content

### Content Moved

Use [301 redirect](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers/redirects) in `nuxt.config.ts`:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  routeRules: {
    '/old-product': { redirect: { to: '/new-product', statusCode: 301 } },
    '/old-category/**': { redirect: { to: '/new-category/**', statusCode: 301 } }
  }
})
```

### Content Permanently Removed

Return 404 or 410. If similar content exists, redirect to relevant category:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  routeRules: {
    // ✅ Good - redirect to relevant category
    '/discontinued-product': { redirect: { to: '/products/similar-items', statusCode: 301 } },

    // ✅ Also good - let Nuxt return 404 naturally for non-existent routes
  }
})
```

## Testing 404 Responses

Verify proper status codes before deploying:

**Browser DevTools:**

1. Open Network tab
2. Navigate to non-existent URL
3. Check status code in response headers
4. Should show `404` not `200`

**Command line:**

```bash
curl -I https://example.com/fake-page

# Output should show:
# HTTP/1.1 404 Not Found
```

**Google Search Console:**

1. Use URL Inspection tool
2. Enter 404 URL
3. Run a live inspection to check the response
4. Compare the reported status with your direct HTTP check
5. Monitor the Page indexing report for soft 404 flags

**Lighthouse:**
Run Lighthouse audit, check "Crawling and Indexing" section for status code issues.

## Common Mistakes

### Redirecting All 404s to Homepage

Creates soft 404 risk. Google may ignore [redirects](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers/redirects) to irrelevant pages.

```ts
// ❌ Bad - mass redirect to homepage
export default defineNuxtConfig({
  routeRules: {
    '/**': { redirect: '/' } // Don't do this
  }
})
```

Only redirect if replacement content is relevant. Otherwise let Nuxt return proper 404.

### Not Using createError()

Rendering error content without throwing error returns `200 OK`:

```vue
<!-- ❌ Bad - returns 200 status -->
<template>
  <div v-if="!product">
    <h1>404 Not Found</h1>
  </div>
</template>
```

```vue
<!-- ✅ Good - returns 404 status -->
<script setup lang="ts">
const { data: product, error } = await useFetch('/api/product')

if (error.value)
  throw error.value
if (!product.value) {
  throw createError({ statusCode: 404 })
}
</script>
```

### Forgetting noindex Meta Tag

If 404 page accidentally returns `200 OK`, [`noindex`](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers/meta-tags) prevents indexing:

```vue [app/error.vue]
<script setup lang="ts">
useHead({
  meta: [
    { name: 'robots', content: 'noindex' }
  ]
})
</script>
```

Safety net, not primary solution. Use `createError()`{lang="ts"} for proper status codes.

Repeated 404s can come from broken links, typos, or automated probes. Inspect referrers and logs before identifying the cause. Repair broken internal links to required destinations.

## Catch-All Routes

A catch-all can look up an app-owned Content collection before throwing a missing-page error:

```vue [pages/[...slug\\].vue]
<script setup lang="ts">
// This catches all unmatched routes
const route = useRoute()

// Try to find content
const { data: page, error } = await useAsyncData(`page-${route.path}`, () =>
  queryCollection('pages').path(route.path).first())

if (error.value)
  throw error.value
if (!page.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Page not found'
  })
}

useSeoMeta({
  title: page.value.title,
  description: page.value.description
})
</script>

<template>
  <ContentRenderer :value="page" />
</template>
```

This initial-load integration excerpt requires a configured `pages` Content collection with title and description fields. Handle route changes separately if the component remains mounted.

## Checklist

::checklist{#nuxt-404-pages}
- The originating page or server throws `createError()`{lang="ts"} for missing data; `error.vue` displays that error
- Dynamic routes (`pages/products/[id].vue`) check for missing data and call `createError()`{lang="ts"} before rendering
- No route redirects blanket-forward to the homepage; each 404 either stays a 404 or gets a targeted 301
- `noindex` on `error.vue` is a fallback only, not a substitute for the correct status code
- `curl -I` against a deleted URL returns `404`, not `200`
- Search Console's Page Indexing report shows no soft 404s
::

## Sitemap

See the full [sitemap](https://nuxtseo.com/sitemap.md) for all pages.
