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).
import express from 'express'
const app = express()
// Mass redirects to homepage lose link equity
app.get('/blog/*', (req, res) => res.redirect(301, '/'))
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'))
Circular redirects break your site:
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'))
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'))
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.