301 redirects pass nearly 100% of link equity to the new URL (Google confirms), preserving your SEO value when content moves. Server-side implementation required for search engines to recognize them.
Use for permanent moves: site migrations, URL restructuring, domain changes, deleted pages with replacements. For duplicate content use canonical tags. For temporary moves use 302 redirects.
In Vue applications, implement redirects at the server level:
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}`)
})
// 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()
})
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)
}
})
301 (Permanent) - Transfers ~100% of link equity to new URL (Google)
302 (Temporary) - Keeps SEO value on original URL
If a 302 stays active for months with no plans to revert, switch to 301 (SEO Clarity). Search engines may eventually treat long-term 302s as permanent anyway.
307/308 - Like 302/301 but preserve HTTP method (POST remains POST). Rarely needed for typical SEO work.
Google recommends keeping 301 redirects active for at least one year (Search Central). This ensures Google transfers all ranking signals and recrawls links pointing to old URLs.
Keep redirects longer if:
Removing redirects before Google processes them loses the transferred SEO value permanently.
Redirect chains (A → B → C) waste crawl budget and slow page speed (Gotch SEO). Each hop degrades Core Web Vitals, particularly LCP and TTFB.
Google follows up to 5 redirect hops, then aborts (Hike SEO). Redirect directly to final destination:
Bad:
/old → /interim → /final
Good:
/old → /final
/interim → /final
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()
})
// 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()
})
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)
}
})
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}`)
})
// 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()
})
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)
}
})
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()
})
// 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()
})
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)
}
})
Learn more about HTTPS in our security guide.
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()
})
// 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()
})
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)
}
})
Verify redirects work correctly before deploying:
curl -I https://example.com/old-page shows redirect headersRedirecting deleted pages to your homepage damages SEO and user experience. Google may treat this as a soft 404, ignoring link equity transfer (Victorious).
// ❌ Bad - mass redirects to homepage
app.get('/blog/*', (req, res) => res.redirect(301, '/'))
// ✅ Good - redirect to relevant content
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'))
// ❌ Bad
app.use((req, res, next) => {
if (req.path.startsWith('/blog/'))
return res.redirect(301, '/')
next()
})
// ✅ Good
app.use((req, res, next) => {
if (req.path === '/blog/vue-tips')
return res.redirect(301, '/articles/vue-tips')
if (req.path === '/blog/seo-guide')
return res.redirect(301, '/articles/seo-guide')
next()
})
import { defineEventHandler, sendRedirect } from 'h3'
// ❌ Bad
export default defineEventHandler((event) => {
if (event.path.startsWith('/blog/'))
return sendRedirect(event, '/', 301)
})
// ✅ Good
export default defineEventHandler((event) => {
if (event.path === '/blog/vue-tips')
return sendRedirect(event, '/articles/vue-tips', 301)
if (event.path === '/blog/seo-guide')
return sendRedirect(event, '/articles/seo-guide', 301)
})
Circular redirects break your site:
// ❌ Bad - 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'))
// ✅ Good - 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'))
// ❌ Bad - creates infinite loop
app.use((req, res, next) => {
if (req.path === '/page-a')
return res.redirect(301, '/page-b')
if (req.path === '/page-b')
return res.redirect(301, '/page-a')
next()
})
// ✅ Good - both redirect to final destination
app.use((req, res, next) => {
if (req.path === '/page-a')
return res.redirect(301, '/final')
if (req.path === '/page-b')
return res.redirect(301, '/final')
next()
})
import { defineEventHandler, sendRedirect } from 'h3'
// ❌ Bad - creates infinite loop
export default defineEventHandler((event) => {
if (event.path === '/page-a')
return sendRedirect(event, '/page-b', 301)
if (event.path === '/page-b')
return sendRedirect(event, '/page-a', 301)
})
// ✅ Good - both redirect to final destination
export default defineEventHandler((event) => {
if (event.path === '/page-a')
return sendRedirect(event, '/final', 301)
if (event.path === '/page-b')
return sendRedirect(event, '/final', 301)
})
JavaScript redirects don't pass link equity reliably. Search engines may not execute JavaScript before indexing. 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 to new URLs, keep redirects for external links and old bookmarks.
If you're using Nuxt, check out Nuxt SEO which handles much of this automatically.
Canonical Link Tag
Canonical URLs tell search engines which version of a page to index when duplicate content exists. Here's how to set them up in Vue.
Duplicate Content
Duplicate content wastes crawl budget and splits ranking signals. Here's how to find and fix it with canonical tags, redirects, and parameter handling.