Skip to content

Commit 410baff

Browse files
stsewdericholscher
andauthored
Redirects: allow to redirect even if a page exists (#9243)
Co-authored-by: Eric Holscher <[email protected]>
1 parent 85b0d1e commit 410baff

File tree

10 files changed

+466
-65
lines changed

10 files changed

+466
-65
lines changed

docs/user/user-defined-redirects.rst

+51-21
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,39 @@ User-defined Redirects
33

44
You can set up redirects for a project in your project dashboard's :guilabel:`Redirects` page.
55

6-
Quick Summary
6+
.. contents:: Table of contents
7+
:local:
8+
9+
Quick summary
710
-------------
811

9-
* Log into your readthedocs.org account.
10-
* From your dashboard, select the project on which you wish to add redirects.
11-
* From the project's top navigation bar, select the :guilabel:`Admin` tab.
12+
* Go to the :guilabel:`Admin` tab of your project.
1213
* From the left navigation menu, select :guilabel:`Redirects`.
13-
* In the form box "Redirect Type" select the type of redirect you want. See below for detail.
14-
* Depending on the redirect type you select, enter FROM and/or TO URL as needed.
14+
* In the form box "Redirect Type" select the type of redirect you want.
15+
:ref:`See below <user-defined-redirects:redirect types>` for detail.
16+
* Depending on the redirect type you select, enter ``From URL`` and/or ``To URL`` as needed.
1517
* When finished, click the :guilabel:`Add` button.
1618

1719
Your redirects will be effective immediately.
1820

19-
Limitations
20-
~~~~~~~~~~~
21-
22-
Redirects are only implemented in case of a *404 File Not Found* error.
23-
If you need to redirect a large number of files that still exist,
24-
please reach out to :doc:`/support`.
21+
Features
22+
--------
2523

26-
Page & Exact Redirects can redirect to URLs outside Read the Docs.
27-
Define the `To URL` as the absolute URL you want to redirect to.
24+
- By default, redirects are followed only if the requested page doesn't exist
25+
(*404 File Not Found* error), if you need to apply a redirect for files that exist,
26+
mark the :guilabel:`Force redirect` option.
27+
**This option is only available on some plan levels**.
28+
Please ask support if you need it for some reason.
29+
- :ref:`user-defined-redirects:page redirects` and :ref:`user-defined-redirects:exact redirects`
30+
can redirect to URLs outside Read the Docs,
31+
just include the protocol in ``To URL``, e.g ``https://example.com``.
2832

29-
Redirect Types
33+
Redirect types
3034
--------------
3135

3236
We offer a few different type of redirects based on what you want to do.
3337

34-
Prefix Redirects
38+
Prefix redirects
3539
~~~~~~~~~~~~~~~~
3640

3741
The most useful and requested feature of redirects was when migrating to Read the Docs from an old host.
@@ -63,7 +67,7 @@ Where ``en`` and ``latest`` are the default language and version values for your
6367
which will prepend ``/$lang/$version/`` to all incoming URLs.
6468

6569

66-
Page Redirects
70+
Page redirects
6771
~~~~~~~~~~~~~~
6872

6973
A more specific case is when you move a page around in your docs.
@@ -82,14 +86,23 @@ You would set the following configuration::
8286
Because of this,
8387
the ``/`` at the start of the ``From URL`` doesn't include the ``/$lang/$version`` prefix (e.g.
8488
``/en/latest``), but just the version-specific part of the URL.
85-
If you want to set directs only for some languages or some versions, you should use
89+
If you want to set redirects only for some languages or some versions, you should use
8690
:ref:`user-defined-redirects:exact redirects` with the fully-specified path.
8791

88-
Exact Redirects
92+
Exact redirects
8993
~~~~~~~~~~~~~~~
9094

9195
*Exact Redirects* are for redirecting a single URL,
92-
and take into account the full URL (including language & version).
96+
taking into account the full URL (including language and version).
97+
98+
You can also redirect a subset of URLs by including the ``$rest`` keyword
99+
at the end of the ``From URL``.
100+
101+
Exact redirects examples
102+
^^^^^^^^^^^^^^^^^^^^^^^^
103+
104+
Redirecting a single URL
105+
````````````````````````
93106

94107
Say you're moving ``docs.example.com`` to Read the Docs and want to redirect traffic
95108
from an old page at ``https://docs.example.com/dev/install.html`` to a new URL
@@ -109,6 +122,9 @@ Your users query would now redirect in the following manner::
109122
Note that you should insert the desired language for "en" and version for "latest" to
110123
achieve the desired redirect.
111124

125+
Redirecting a whole sub-path to a different one
126+
```````````````````````````````````````````````
127+
112128
*Exact Redirects* could be also useful to redirect a whole sub-path to a different one by using a special ``$rest`` keyword in the "From URL".
113129
Let's say that you want to redirect your readers of your version ``2.0`` of your documentation under ``/en/2.0/`` because it's deprecated,
114130
to the newest ``3.0`` version of it at ``/en/3.0/``.
@@ -128,8 +144,22 @@ Similarly, if you maintain several branches of your documentation (e.g. ``3.0``
128144
``latest``) and decide to move pages in ``latest`` but not the older branches, you can use
129145
*Exact Redirects* to do so.
130146

147+
Migrating your documentation to another domain
148+
``````````````````````````````````````````````
149+
150+
You can use an exact redirect to migrate your documentation to another domain,
151+
for example::
152+
153+
Type: Exact Redirect
154+
From URL: /$rest
155+
To URL: https://newdocs.example.com/
156+
Force Redirect: True
157+
158+
Then all pages will redirect to the new domain, for example
159+
``https://docs.example.com/en/latest/install.html`` will redirect to
160+
``https://newdocs.example.com/en/latest/install.html``.
131161

132-
Sphinx Redirects
162+
Sphinx redirects
133163
~~~~~~~~~~~~~~~~
134164

135165
We also support redirects for changing the type of documentation Sphinx is building.

readthedocs/projects/forms.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -585,21 +585,30 @@ class RedirectForm(forms.ModelForm):
585585

586586
class Meta:
587587
model = Redirect
588-
fields = ['redirect_type', 'from_url', 'to_url']
588+
fields = ["redirect_type", "from_url", "to_url", "force"]
589589

590590
def __init__(self, *args, **kwargs):
591591
self.project = kwargs.pop('project', None)
592592
super().__init__(*args, **kwargs)
593593

594+
if self.project.has_feature(Feature.ALLOW_FORCED_REDIRECTS):
595+
# Remove the nullable option from the form.
596+
# TODO: remove after migration.
597+
self.fields["force"].widget = forms.CheckboxInput()
598+
self.fields["force"].empty_value = False
599+
else:
600+
self.fields.pop("force")
601+
594602
def save(self, **_): # pylint: disable=arguments-differ
595603
# TODO this should respect the unused argument `commit`. It's not clear
596604
# why this needs to be a call to `create`, instead of relying on the
597605
# super `save()` call.
598606
redirect = Redirect.objects.create(
599607
project=self.project,
600-
redirect_type=self.cleaned_data['redirect_type'],
601-
from_url=self.cleaned_data['from_url'],
602-
to_url=self.cleaned_data['to_url'],
608+
redirect_type=self.cleaned_data["redirect_type"],
609+
from_url=self.cleaned_data["from_url"],
610+
to_url=self.cleaned_data["to_url"],
611+
force=self.cleaned_data.get("force", False),
603612
)
604613
return redirect
605614

readthedocs/projects/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,7 @@ def add_features(sender, **kwargs):
17741774
CDN_ENABLED = "cdn_enabled"
17751775
DOCKER_GVISOR_RUNTIME = "gvisor_runtime"
17761776
RECORD_404_PAGE_VIEWS = "record_404_page_views"
1777+
ALLOW_FORCED_REDIRECTS = "allow_forced_redirects"
17771778

17781779
# Versions sync related features
17791780
SKIP_SYNC_TAGS = 'skip_sync_tags'
@@ -1865,6 +1866,10 @@ def add_features(sender, **kwargs):
18651866
RECORD_404_PAGE_VIEWS,
18661867
_("Record 404s page views."),
18671868
),
1869+
(
1870+
ALLOW_FORCED_REDIRECTS,
1871+
_("Allow forced redirects."),
1872+
),
18681873

18691874
# Versions sync related features
18701875
(

readthedocs/proxito/tests/test_full.py

+18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from readthedocs.projects.models import Domain, Feature, Project
2828
from readthedocs.proxito.views.mixins import ServeDocsMixin
29+
from readthedocs.redirects.models import Redirect
2930
from readthedocs.rtd_tests.storage import BuildMediaFileSystemStorageTest
3031
from readthedocs.subscriptions.models import Plan, PlanFeature, Subscription
3132

@@ -1223,6 +1224,23 @@ def _test_cache_control_header_project(self, expected_value, host=None):
12231224
self.assertEqual(resp.headers['CDN-Cache-Control'], 'private', url)
12241225
self.assertNotIn('Cache-Tag', resp.headers, url)
12251226

1227+
# Forced redirects will be cached only if the version is public.
1228+
get(
1229+
Redirect,
1230+
project=self.project,
1231+
redirect_type="exact",
1232+
from_url="/en/latest/install.html",
1233+
to_url="/en/latest/tutorial/install.html",
1234+
force=True,
1235+
)
1236+
url = "/en/latest/install.html"
1237+
resp = self.client.get(url, secure=True, HTTP_HOST=host)
1238+
self.assertEqual(
1239+
resp["Location"], f"https://{host}/en/latest/tutorial/install.html", url
1240+
)
1241+
self.assertEqual(resp.headers["CDN-Cache-Control"], expected_value, url)
1242+
self.assertEqual(resp.headers["Cache-Tag"], "project,project:latest", url)
1243+
12261244
def _test_cache_control_header_subproject(self, expected_value, host=None):
12271245
"""
12281246
Test the CDN-Cache-Control header on requests for `self.subproject`.

0 commit comments

Comments
 (0)