Search engines read HTTP status codes as instructions. Wrong codes leak link equity or keep dead pages indexed.
Permanent moves — in next.config.mjs (returns 308, treated like 301 by Google):
// next.config.mjs
export default {
async redirects() {
return [
{
source: "/old-blog/:slug",
destination: "/blog/:slug",
permanent: true,
},
];
},
};
In-app redirects — pick the function that matches the semantics:
import { redirect, permanentRedirect, notFound } from "next/navigation";
// 307 — temporary (login gates, A/B routing)
redirect("/login");
// 308 — permanent (slug changed, old URL should transfer authority)
permanentRedirect(`/blog/${post.newSlug}`);
// Real 404 with your not-found.tsx UI
if (!post) notFound();
Rules that keep the index clean:
- No redirect chains: old → older → current wastes crawl budget and dilutes signals. Always redirect straight to the final URL.
- Missing content returns 404, not a 200 "nothing found" page (a soft 404 — Google flags these in Search Console).
- Pick one URL shape and enforce it: with or without trailing slash (
trailingSlashin config), lowercase, no duplicate www/non-www (redirect at the edge/DNS level). - When you change a slug, add the redirect in the same PR — retroactive redirect archaeology never happens.
Replatforming? The full playbook — inventory, redirect map, parity checks and launch monitoring — is in SEO migration.
Checklist:
- All legacy URLs 308/301-redirect directly to final destinations (no chains)
- Custom
not-found.tsxthat genuinely returns 404 - One canonical URL shape (slash, case, host) enforced globally
- Slug changes always ship with their redirect