Part 9 of 22

Redirects, 404s and status codes

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 (trailingSlash in 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.tsx that genuinely returns 404
  • One canonical URL shape (slash, case, host) enforced globally
  • Slug changes always ship with their redirect

Want this handled for you?

I audit and fix technical SEO on Next.js sites — indexing, performance, hreflang and AI visibility.

Technical SEO service