DomainLens

Guides

Redirect in PHP, JS and Django: Correct Status Codes for SEO

A redirect is a status code plus a Location header. Every framework defaults to a temporary redirect, so the permanent case almost always has to be stated explicitly.

Check your site before you start fixing

Run a fresh DomainLens audit and use the report as your priority list.

Run a free SEO audit

Redirect meaning: what the status code decides

A redirect is a server response that says the resource lives at another address. Two parts do the work: the status code, which states whether the move is permanent, and the Location header, which gives the new address. Everything else — the framework, the language, the syntax — is a wrapper around those two facts.

The status code is the part with SEO consequences, and it is the part frameworks get wrong by default. A permanent move sent as 302 leaves the old URL indexed and the signals unconsolidated, which is precisely the outcome a migration is supposed to avoid. Decide the code before writing any code.

CodeMeaningUse when
301Moved permanentlyThe URL changed for good; consolidate signals
302Found (temporary)A genuinely temporary detour; keep the original indexed
307Temporary, method preservedA temporary redirect that must not turn POST into GET
308Permanent, method preservedA permanent move where the method must survive
200 plus rewriteNot a redirect at allServing different content at the same URL

Redirect in PHP and Laravel

Plain PHP sends 302 unless you pass a third argument, and forgetting to stop execution lets the rest of the script run and emit output after the header. Both mistakes are invisible in a browser, which follows the redirect either way, so they survive testing and reach production intact.

Laravel wraps this in a redirect helper whose second argument is the status code. The same default applies: omit it and you have shipped a temporary redirect.

  • Send the redirect before any output; a stray blank line before the opening tag breaks the header.
  • Redirect to an absolute, canonical URL with the correct host, scheme, and trailing-slash form.
  • Never redirect into another redirect — each hop costs crawl time and dilutes the chain.
  • Validate user-supplied redirect targets against an allowlist. An open redirect is a security issue before it is an SEO one.
Permanent redirects in PHP and Laravel
<?php
// Plain PHP — the third argument is the status code.
// Without it you send 302; without exit the script continues.
header('Location: https://example.com/new-path', true, 301);
exit;

// Laravel — second argument is the status code
return redirect('/new-path', 301);

// Named route, permanent
return redirect()->route('guides.show', ['slug' => 'x'], 301);

// Leaving the application entirely
return redirect()->away('https://partner.example/offer', 301);

Redirect in JS, and why it is weaker

A JavaScript redirect is not an HTTP redirect. The server returns 200, the browser downloads and executes the script, and only then navigates. Google does follow these after rendering, but the signal is weaker, slower, and entirely dependent on the script running at all.

Use one only when you cannot control the server response — inside a static host with no redirect rules, for example. If you can send a header, send a header. A meta refresh is worse still: no status code, no Location header, and a delay the user can see.

  • Prefer replace() over href so the old URL does not sit in the back stack and trap the user.
  • Never rely on a JS redirect for a site migration — use server-side 301s.
  • Remember that a blocked, failed, or slow script means no redirect happens at all.
Client-side navigation options
// replace() leaves no history entry — closest to a redirect
window.location.replace('https://example.com/new-path');

// href pushes a history entry; Back returns to the old URL
window.location.href = 'https://example.com/new-path';

<!-- Meta refresh: no status code, worst option, avoid -->
<meta http-equiv="refresh" content="0; url=/new-path">

Redirect in Django

Django separates the temporary and permanent cases into distinct response classes, and the redirect shortcut takes a permanent flag. The framework also applies implicit redirects of its own, which is where the surprise hops usually come from.

Three settings each add a hop when enabled: APPEND_SLASH, PREPEND_WWW, and SECURE_SSL_REDIRECT. Combine them and a plain HTTP request to a slash-less, non-www URL travels three redirects before it reaches content.

  • APPEND_SLASH redirects a slash-less URL to the slashed form — pick one form and keep internal links consistent with it.
  • PREPEND_WWW adds a hop; if your hosting already normalises the hostname you now have two doing the same job.
  • Handle the scheme and hostname at the edge where possible, so Django only handles genuine application redirects.
Django redirect forms
from django.shortcuts import redirect
from django.http import HttpResponsePermanentRedirect
from django.views.generic import RedirectView

# Shortcut — temporary by default (302)
def view(request):
    return redirect('/new-path')

# Shortcut — permanent (301)
def view(request):
    return redirect('/new-path', permanent=True)

# Explicit response class (301)
def view(request):
    return HttpResponsePermanentRedirect('/new-path')

# URL-level, no view needed
path('old-path/', RedirectView.as_view(
    url='/new-path/', permanent=True,
))

Redirect checker: verifying what you shipped

A redirect that works in a browser can still be wrong in three ways the browser hides: the wrong status code, an extra hop, and a final destination that is not the canonical URL. Checking means reading the raw response rather than watching the address bar settle.

  1. 1Request the old URL without following redirects and read the status line and Location header directly.
  2. 2Follow the full chain and count the hops. One is correct, two is tolerable, three needs collapsing.
  3. 3Confirm the final URL returns 200 and is the canonical form — not another redirect, and not a soft 404.
  4. 4Test from a cold cache and a non-browser client; a CDN or service worker can mask the real server response.
  5. 5After a migration, watch the old URLs in Search Console until they report as redirected rather than indexed.
Checking a redirect from the command line
# Status and Location only, without following
curl -sI https://example.com/old-path | head -n 5

# Follow the chain and print the final destination
curl -sIL https://example.com/old-path \
  -o /dev/null -w '%{http_code} %{url_effective}\n'

# Every hop, with intermediate URLs
curl -sIL https://example.com/old-path | grep -Ei '^(HTTP|location)'

How DomainLens contributes

DomainLens reports the status code, the redirect chain, and the final destination for the URLs it audits, so a stray 302 or an accidental three-hop chain surfaces without manual curl work. For collapsing chains see redirect chains, for the wider status code set see the status code reference, and for a whole-site move see the migration checklist.

Does a 301 redirect pass full ranking signals?
Google has said no PageRank is lost through 301 or 302 redirects. The practical reason to prefer 301 for a permanent move is that it states intent clearly and consolidates the two URLs into one.
Is a JavaScript redirect bad for SEO?
It is weaker rather than forbidden. Google follows it after rendering, but it depends on script execution and carries no status code, so a server-side redirect is always preferable when available.
How long should redirects stay in place after a migration?
At least a year. Google needs repeated crawls to consolidate the signals, and external links keep sending traffic to old URLs long after that.
What is the difference between 301 and 308?
Both are permanent. 308 guarantees the request method is preserved, so a POST stays a POST, while 301 historically allowed clients to convert it to GET.

Related resources