From 43b9dcb59cf40bdba5955f4d5d3f0f1800277da7 Mon Sep 17 00:00:00 2001 From: Vincent Ngobeh Date: Thu, 4 Dec 2025 16:46:15 +0000 Subject: [PATCH] Fix non-docs pages accessible on docs subdomain Redirect /foundation/ and /community/ paths from docs.djangoproject.com to www.djangoproject.com with a 301 permanent redirect. Fixes #878 --- djangoproject/urls/docs.py | 69 +++++++++++++++++++++++++++----------- docs/tests/test_views.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/djangoproject/urls/docs.py b/djangoproject/urls/docs.py index 556861af40..c5edafa96d 100644 --- a/djangoproject/urls/docs.py +++ b/djangoproject/urls/docs.py @@ -1,8 +1,10 @@ from collections.abc import MutableMapping +from django.conf import settings from django.contrib.sitemaps.views import sitemap -from django.http import HttpResponse -from django.urls import include, path +from django.http import HttpResponse, HttpResponsePermanentRedirect +from django.urls import include, path, re_path +from django.views import View from docs.models import DocumentRelease from docs.sitemaps import DocsSitemap @@ -41,21 +43,50 @@ def __setitem__(key, value): sitemaps = Sitemaps() -urlpatterns = docs_urlpatterns + [ - path("sitemap.xml", sitemap_index, {"sitemaps": sitemaps}), - path( - "sitemap-
.xml", - sitemap, - {"sitemaps": sitemaps}, - name="document-sitemap", - ), - path( - "google79eabba6bf6fd6d3.html", - lambda req: HttpResponse( - "google-site-verification: google79eabba6bf6fd6d3.html" + +class WwwRedirectView(View): + """ + Redirect requests to the www subdomain, preserving the path. + + This is used to redirect non-documentation pages (like /foundation/ and + /community/) that are incorrectly accessible on docs.djangoproject.com + to their canonical location on www.djangoproject.com. + """ + + def get(self, request, *args, **kwargs): + scheme = getattr(settings, "HOST_SCHEME", "https") + parent_host = getattr(settings, "PARENT_HOST", "djangoproject.com") + redirect_url = f"{scheme}://www.{parent_host}{request.path}" + if request.META.get("QUERY_STRING"): + redirect_url = f"{redirect_url}?{request.META['QUERY_STRING']}" + return HttpResponsePermanentRedirect(redirect_url) + + +urlpatterns = ( + [ + # Redirect non-documentation pages to the www subdomain. + # These pages should not be accessible on docs.djangoproject.com. + # See https://github.com/django/djangoproject.com/issues/878 + re_path(r"^foundation(?:/(?P.*))?$", WwwRedirectView.as_view()), + re_path(r"^community(?:/(?P.*))?$", WwwRedirectView.as_view()), + ] + + docs_urlpatterns + + [ + path("sitemap.xml", sitemap_index, {"sitemaps": sitemaps}), + path( + "sitemap-
.xml", + sitemap, + {"sitemaps": sitemaps}, + name="document-sitemap", + ), + path( + "google79eabba6bf6fd6d3.html", + lambda req: HttpResponse( + "google-site-verification: google79eabba6bf6fd6d3.html" + ), ), - ), - # This just exists to make sure we can proof that the error pages work - # under both hostnames. - path("", include("legacy.urls")), -] + # This just exists to make sure we can proof that the error pages work + # under both hostnames. + path("", include("legacy.urls")), + ] +) diff --git a/docs/tests/test_views.py b/docs/tests/test_views.py index a759793ba4..1e250c1d7d 100644 --- a/docs/tests/test_views.py +++ b/docs/tests/test_views.py @@ -40,6 +40,72 @@ def test_internals_team(self): fetch_redirect_response=False, ) + def test_foundation_redirects_to_www(self): + """Foundation pages on docs subdomain should redirect to www.""" + response = self.client.get( + "/foundation/", + headers={"host": "docs.djangoproject.localhost:8000"}, + ) + self.assertRedirects( + response, + "http://www.djangoproject.localhost:8000/foundation/", + status_code=HTTPStatus.MOVED_PERMANENTLY, + fetch_redirect_response=False, + ) + + def test_foundation_subpage_redirects_to_www(self): + """Foundation subpages on docs subdomain should redirect to www.""" + response = self.client.get( + "/foundation/records/minutes/2017-02-09/", + headers={"host": "docs.djangoproject.localhost:8000"}, + ) + self.assertRedirects( + response, + "http://www.djangoproject.localhost:8000" + "/foundation/records/minutes/2017-02-09/", + status_code=HTTPStatus.MOVED_PERMANENTLY, + fetch_redirect_response=False, + ) + + def test_community_redirects_to_www(self): + """Community pages on docs subdomain should redirect to www.""" + response = self.client.get( + "/community/", + headers={"host": "docs.djangoproject.localhost:8000"}, + ) + self.assertRedirects( + response, + "http://www.djangoproject.localhost:8000/community/", + status_code=HTTPStatus.MOVED_PERMANENTLY, + fetch_redirect_response=False, + ) + + def test_community_subpage_redirects_to_www(self): + """Community subpages on docs subdomain should redirect to www.""" + response = self.client.get( + "/community/logos/", + headers={"host": "docs.djangoproject.localhost:8000"}, + ) + self.assertRedirects( + response, + "http://www.djangoproject.localhost:8000/community/logos/", + status_code=HTTPStatus.MOVED_PERMANENTLY, + fetch_redirect_response=False, + ) + + def test_redirect_preserves_query_string(self): + """Redirects should preserve query strings.""" + response = self.client.get( + "/foundation/?page=2&sort=date", + headers={"host": "docs.djangoproject.localhost:8000"}, + ) + self.assertRedirects( + response, + "http://www.djangoproject.localhost:8000/foundation/?page=2&sort=date", + status_code=HTTPStatus.MOVED_PERMANENTLY, + fetch_redirect_response=False, + ) + class SearchFormTestCase(TestCase): fixtures = ["doc_test_fixtures"]