---
title: "HTTP Redirects for SEO in Vue"
description: "Implement server-side 301 redirects in Vue SSR (Express, Vite, H3) so migrated pages keep their rankings and avoid redirect chains."
canonical_url: "https://nuxtseo.com/learn-seo/vue/controlling-crawlers/redirects"
last_updated: "2026-07-16"
---

<key-takeaways>

- 301 redirects transfer nearly all link equity to the new URL, so use them for permanent moves rather than temporary ones
- Redirects must run server-side; client-side JavaScript redirects don't pass SEO value
- Avoid redirect chains: point every old URL straight at its final destination instead of hopping through intermediate pages
- Keep a redirect live for at least a year after a migration so Google's systems see it enough times to treat it as permanent

</key-takeaways>

A 301 redirect tells search engines a page has moved permanently and transfers nearly all of its link equity to the new URL. Get the migration wrong (missing redirects, chains, or redirects pointing at the wrong page) and you lose the rankings that page built up.

Redirects work the same way for AI crawlers as they do for search bots: GPTBot, PerplexityBot, and ClaudeBot follow HTTP redirects at the protocol level, since redirects happen before any JavaScript runs. Migrate cleanly and any citations your pages earn in [ChatGPT](https://chatgpt.com) or [Perplexity](https://perplexity.ai) carry over along with your search rankings.

Use 301s for permanent moves: site migrations, URL restructuring, domain changes, and deleted pages with a direct replacement. For duplicate content use [canonical tags](/learn-seo/vue/controlling-crawlers/canonical-urls) instead. For temporary moves use a 302.

## Quick Setup

In a Vue application, implement redirects at the server level:

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page')
})

app.get('/blog/:slug', (req, res) => {
  res.redirect(301, `/articles/${req.params.slug}`)
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use((req, res, next) => {
  if (req.path === '/old-page') {
    return res.redirect(301, '/new-page')
  }
  if (req.path.startsWith('/blog/')) {
    const slug = req.path.replace('/blog/', '')
    return res.redirect(301, `/articles/${slug}`)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, sendRedirect } from 'h3'

export default defineEventHandler((event) => {
  if (event.path === '/old-page') {
    return sendRedirect(event, '/new-page', 301)
  }
  if (event.path.startsWith('/blog/')) {
    const slug = event.path.replace('/blog/', '')
    return sendRedirect(event, `/articles/${slug}`, 301)
  }
})
```

</code-group>

## 301 vs 302 Redirects

### When to Use Each

**301 (Permanent)**: transfers nearly all link equity to the new URL. John Mueller and Matt Cutts have both confirmed Google forwards PageRank through 301s much like it does through a normal link ([Search Engine Journal](https://www.searchenginejournal.com/301-redirect-pagerank/275503/)). Use it for:

- Permanent content moves
- Domain migrations
- URL structure changes
- Deleted pages with direct replacements
- HTTP to HTTPS upgrades

**302 (Temporary)**: keeps SEO value on the original URL. Use it for:

- A/B testing
- Temporary promotions
- Maintenance pages
- Out-of-stock product redirects

If a 302 stays active for months with no plan to revert, switch to a 301: Google only treats permanent redirects as a canonicalization signal, telling its indexing pipeline that the destination URL should be the one it indexes ([Search Central](https://developers.google.com/search/docs/crawling-indexing/301-redirects)).

**307/308**: like 302/301 but preserve the HTTP method (a POST stays a POST). Rarely needed for typical SEO work.

### How Long to Keep Redirects Active

John Mueller recommends keeping a 301 redirect active for at least a year after a migration, since Google's systems need to see the redirect several times before treating the move as permanent ([Search Engine Journal](https://www.searchenginejournal.com/google-keep-301-redirects-in-place-for-a-year/428998/)).

Keep redirects even longer if:

- External sites still link to the old URLs
- Old URLs still receive referral traffic
- High-value pages have many backlinks

Remove a redirect before Google has fully processed it and you lose the transferred SEO value for good.

### Avoid Redirect Chains

Redirect chains (A → B → C) waste [crawl budget](/learn-seo/vue/controlling-crawlers#crawler-budget) and slow page speed; each hop degrades [Core Web Vitals](/learn-seo/vue/launch-and-listen/core-web-vitals), particularly LCP and TTFB. Google's crawlers follow up to [10 redirect hops](https://developers.google.com/search/docs/crawling-indexing/http-network-errors#3xx-redirection) by default, then give up. Redirect directly to the final destination:

Bad:

```plaintext
/old → /interim → /final
```

Good:

```plaintext
/old → /final
/interim → /final
```

## Common Patterns

### Domain Migration

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')
  if (host === 'old-domain.com') {
    return res.redirect(301, `https://new-domain.com${req.path}`)
  }
  next()
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')
  if (host === 'old-domain.com') {
    return res.redirect(301, `https://new-domain.com${req.path}`)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, getRequestHost, sendRedirect } from 'h3'

export default defineEventHandler((event) => {
  const host = getRequestHost(event)
  if (host === 'old-domain.com') {
    return sendRedirect(event, `https://new-domain.com${event.path}`, 301)
  }
})
```

</code-group>

### URL Structure Changes

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.get('/old', (req, res) => {
  res.redirect(301, '/new')
})

app.get('/blog/:slug', (req, res) => {
  res.redirect(301, `/articles/${req.params.slug}`)
})

app.get('/products/:id', (req, res) => {
  res.redirect(301, `/shop/${req.params.id}`)
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use((req, res, next) => {
  if (req.path === '/old') {
    return res.redirect(301, '/new')
  }
  if (req.path.startsWith('/blog/')) {
    const slug = req.path.replace('/blog/', '')
    return res.redirect(301, `/articles/${slug}`)
  }
  if (req.path.startsWith('/products/')) {
    const id = req.path.replace('/products/', '')
    return res.redirect(301, `/shop/${id}`)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, sendRedirect } from 'h3'

export default defineEventHandler((event) => {
  if (event.path === '/old') {
    return sendRedirect(event, '/new', 301)
  }
  if (event.path.startsWith('/blog/')) {
    const slug = event.path.replace('/blog/', '')
    return sendRedirect(event, `/articles/${slug}`, 301)
  }
  if (event.path.startsWith('/products/')) {
    const id = event.path.replace('/products/', '')
    return sendRedirect(event, `/shop/${id}`, 301)
  }
})
```

</code-group>

### HTTPS Enforcement

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(301, `https://${req.get('host')}${req.path}`)
  }
  next()
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(301, `https://${req.get('host')}${req.path}`)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, getHeader, getRequestHost, sendRedirect } from 'h3'

export default defineEventHandler((event) => {
  if (getHeader(event, 'x-forwarded-proto') !== 'https') {
    const host = getRequestHost(event)
    return sendRedirect(event, `https://${host}${event.path}`, 301)
  }
})
```

</code-group>

Learn more about HTTPS in our [security guide](/learn-seo/vue/routes-and-rendering/security#https).

### WWW Standardization

<code-group>

```ts [Express]
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')
  if (!host.startsWith('www.')) {
    return res.redirect(301, `https://www.${host}${req.path}`)
  }
  next()
})
```

```ts [Vite]
// server.js for Vite SSR
import express from 'express'

const app = express()

app.use((req, res, next) => {
  const host = req.get('host')
  if (!host.startsWith('www.')) {
    return res.redirect(301, `https://www.${host}${req.path}`)
  }
  next()
})
```

```ts [H3]
import { defineEventHandler, getRequestHost, sendRedirect } from 'h3'

export default defineEventHandler((event) => {
  const host = getRequestHost(event)
  if (!host.startsWith('www.')) {
    return sendRedirect(event, `https://www.${host}${event.path}`, 301)
  }
})
```

</code-group>

## Testing Redirects

Verify redirects work correctly before deploying:

1. **Check status code**: use browser dev tools Network tab, confirm 301 or 302
2. **Test destination**: make sure the redirect points to the correct final URL
3. **Verify no chains**: confirm a single hop to the destination
4. **Test trailing slashes**: check with and without a trailing slash
5. **Check query parameters**: verify parameters carry over if needed

Useful tools:

- [Google Search Console](https://search.google.com/search-console): monitor crawl errors and redirect issues
- Browser dev tools Network tab: check status codes and headers
- [Screaming Frog](https://www.screamingfrog.co.uk/seo-spider/): bulk redirect testing and chain detection
- curl: `curl -I https://example.com/old-page` shows redirect headers

## Common Mistakes

### Redirecting to Irrelevant Pages

Mass-redirecting deleted pages to your homepage sends users and crawlers to unrelated content. It discards the topical relevance the old URL had built up, and it reads as lazy to anyone who lands there looking for the page they clicked.

<code-group>

```ts [❌ Bad]
import express from 'express'

const app = express()

// Mass redirects to homepage lose link equity
app.get('/blog/*', (req, res) => res.redirect(301, '/'))
```

```ts [✅ Good]
import express from 'express'

const app = express()

// Redirect to relevant content preserves SEO value
app.get('/blog/vue-tips', (req, res) => res.redirect(301, '/articles/vue-tips'))
app.get('/blog/seo-guide', (req, res) => res.redirect(301, '/articles/seo-guide'))
```

</code-group>

### Redirect Loops

Circular redirects break your site:

<code-group>

```ts [❌ Bad]
import express from 'express'

const app = express()

// Creates infinite loop
app.get('/page-a', (req, res) => res.redirect(301, '/page-b'))
app.get('/page-b', (req, res) => res.redirect(301, '/page-a'))
```

```ts [✅ Good]
import express from 'express'

const app = express()

// Both redirect to final destination
app.get('/page-a', (req, res) => res.redirect(301, '/final'))
app.get('/page-b', (req, res) => res.redirect(301, '/final'))
```

</code-group>

### Client-Side Redirects and Stale Internal Links

JavaScript redirects don't pass link equity reliably, and search engines may not execute the JavaScript before indexing the page. Always use server-side redirects (301/302 status codes) for SEO purposes.

Relying on redirects for internal links wastes server resources and slows page speed. Update internal links to point directly at the new URLs, and keep redirects for external links and old bookmarks.

## Checklist

<checklist id="vue-redirects">

- Every permanent URL change uses a server-side 301 redirect
- Redirects run in server middleware (Express, H3, or your framework's equivalent), not client-side JavaScript
- Old URLs redirect directly to their final destination, no chains
- No redirect loops
- Redirects stay active for at least a year after a migration
- Internal links point directly at the new URLs

</checklist>

If you're using Nuxt, [Nuxt SEO](/docs/nuxt-seo/getting-started/introduction) handles redirects through `routeRules` without any server framework wiring. See [redirects in Nuxt](/learn-seo/nuxt/controlling-crawlers/redirects) for the full setup.
