Redirect management: 301 permanent, 302 temporary. Use when: setting up URL redirects, migrating URLs, handling slug changes, preventing 404 errors.
# apps/seo/models.py
class Redirect(TimestampedModel):
source_path = models.CharField(max_length=500, unique=True, db_index=True)
target_path = models.CharField(max_length=500)
status_code = models.PositiveSmallIntegerField(
default=301,
choices=[(301, "301 Permanent"), (302, "302 Temporary")],
)
is_active = models.BooleanField(default=True)
hit_count = models.PositiveIntegerField(default=0)
last_hit = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = "seo_redirect"
ordering = ["-created_at"]
verbose_name = "Redirect"
verbose_name_plural = "Redirects"
def __str__(self) -> str:
return f"{self.source_path} → {self.target_path} ({self.status_code})"
| Status | Use Case | SEO Effect |
|---|---|---|
| 301 | Permanent URL change, slug rename, site migration | Passes ~90-99% link equity |
| 302 | Temporary maintenance, A/B test, seasonal redirect | Does NOT pass link equity |
| 308 | Permanent (preserves HTTP method) | Same as 301 for GET |
# apps/seo/middleware.py
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
from django.utils import timezone
class RedirectMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
from apps.seo.models import Redirect
path = request.path
redirect = Redirect.objects.filter(
source_path=path, is_active=True
).first()
if redirect:
redirect.hit_count += 1
redirect.last_hit = timezone.now()
redirect.save(update_fields=["hit_count", "last_hit"])
if redirect.status_code == 301:
return HttpResponsePermanentRedirect(redirect.target_path)
return HttpResponseRedirect(redirect.target_path)
return self.get_response(request)
# apps/seo/services.py
def detect_redirect_chains(max_depth: int = 5) -> list[list[str]]:
"""Find redirect chains longer than 1 hop."""
from apps.seo.models import Redirect
chains: list[list[str]] = []
for r in Redirect.objects.filter(is_active=True):
chain = [r.source_path]
target = r.target_path
depth = 0
while depth < max_depth:
next_r = Redirect.objects.filter(
source_path=target, is_active=True
).first()
if not next_r:
break
chain.append(next_r.source_path)
target = next_r.target_path
depth += 1
if len(chain) > 1:
chain.append(target)
chains.append(chain)
return chains
def create_redirect_on_slug_change(
old_path: str, new_path: str
) -> None:
"""Create a 301 redirect when a model's slug changes."""
from apps.seo.models import Redirect
if old_path != new_path:
Redirect.objects.update_or_create(
source_path=old_path,
defaults={"target_path": new_path, "status_code": 301, "is_active": True},
)
hit_count tracking — can't identify stale redirects& .\.venv\Scripts\python.exe -m ruff check . --fix
& .\.venv\Scripts\python.exe -m ruff format .
& .\.venv\Scripts\python.exe manage.py check --settings=app.settings_dev