Repository: readthedocs/readthedocs.org
Issue: Sitemap sort order may prioritise unstable/development documentation

In `sitemap.xml`, the sort order should prioritise the user-selected default version (Admin > Versions > Default Version) in the backend instead of setting `latest` to highest priority. In many cases (and also by [definition](https://docs.readthedocs.io/en/stable/versions.html)), `latest` points at development documentation while `stable` is the version most people should be using, and should appear first in search results.

Example from: https://docs.dash.org/sitemap.xml
`stable` should be higher priority than `latest`, but the reverse is true.
```
<url>
  <loc>http://docs.dash.org/en/latest/</loc>
  <changefreq>daily</changefreq>
  <priority>1</priority>
</url>

<url>
  <loc>http://docs.dash.org/en/stable/</loc>
  <changefreq>weekly</changefreq>
  <priority>0.9</priority>
</url>
```

Changed files:
readthedocs/core/views/serve.py, readthedocs/rtd_tests/tests/test_doc_serving.py

diff --git a/readthedocs/core/views/serve.py b/readthedocs/core/views/serve.py
index cd5ab8fdb..237b843e0 100644
--- a/readthedocs/core/views/serve.py
+++ b/readthedocs/core/views/serve.py
@@ -38,6 +38,7 @@ from django.utils.encoding import iri_to_uri
 from django.views.decorators.cache import cache_page
 from django.views.static import serve
 
+from readthedocs.builds.constants import LATEST, STABLE
 from readthedocs.builds.models import Version
 from readthedocs.core.permissions import AdminPermission
 from readthedocs.core.resolver import resolve, resolve_path
@@ -373,14 +374,14 @@ def sitemap_xml(request, project):
         """
         Generator returning ``changefreq`` needed by sitemap.xml.
 
-        It returns ``daily`` on first iteration, then ``weekly`` and then it
+        It returns ``weekly`` on first iteration, then ``daily`` and then it
         will return always ``monthly``.
 
         We are using ``monthly`` as last value because ``never`` is too
         aggressive. If the tag is removed and a branch is created with the same
         name, we will want bots to revisit this.
         """
-        changefreqs = ['daily', 'weekly']
+        changefreqs = ['weekly', 'daily']
         yield from itertools.chain(changefreqs, itertools.repeat('monthly'))
 
     if project.privacy_level == constants.PRIVATE:
@@ -392,6 +393,19 @@ def sitemap_xml(request, project):
             only_active=True,
         ),
     )
+
+    # This is a hack to swap the latest version with
+    # stable version to get the stable version first in the sitemap.
+    # We want stable with priority=1 and changefreq='weekly' and
+    # latest with priority=0.9 and changefreq='daily'
+    # More details on this: https://github.com/rtfd/readthedocs.org/issues/5447
+    if (
+        len(sorted_versions) >= 2 and
+        sorted_versions[0].slug == LATEST and
+        sorted_versions[1].slug == STABLE
+    ):
+        sorted_versions[0], sorted_versions[1] = sorted_versions[1], sorted_versions[0]
+
     versions = []
     for version, priority, changefreq in zip(
             sorted_versions,
diff --git a/readthedocs/rtd_tests/tests/test_doc_serving.py b/readthedocs/rtd_tests/tests/test_doc_serving.py
index 4f6cb9399..fed119bba 100644
--- a/readthedocs/rtd_tests/tests/test_doc_serving.py
+++ b/readthedocs/rtd_tests/tests/test_doc_serving.py
@@ -240,6 +240,15 @@ class TestPublicDocs(BaseDocServing):
             project=self.public,
             active=True
         )
+        stable_version = fixture.get(
+            Version,
+            identifier='stable',
+            verbose_name='stable',
+            slug='stable',
+            privacy_level=constants.PUBLIC,
+            project=self.public,
+            active=True
+        )
         # This also creates a Version `latest` Automatically for this project
         translation = fixture.get(
             Project,
@@ -269,7 +278,7 @@ class TestPublicDocs(BaseDocServing):
                 ),
             )
 
-        # stable is marked as PRIVATE and should not appear here
+        # PRIVATE version should not appear here
         self.assertNotContains(
             response,
             self.public.get_docs_url(
@@ -294,6 +303,28 @@ class TestPublicDocs(BaseDocServing):
         # in language and country value. (zh_CN should be zh-CN)
         self.assertContains(response, 'zh-CN')
 
+        # Check if STABLE version has 'priority of 1 and changefreq of weekly.
+        self.assertEqual(
+            response.context['versions'][0]['loc'],
+            self.public.get_docs_url(
+                version_slug=stable_version.slug,
+                lang_slug=self.public.language,
+                private=False,
+            ),)
+        self.assertEqual(response.context['versions'][0]['priority'], 1)
+        self.assertEqual(response.context['versions'][0]['changefreq'], 'weekly')
+
+        # Check if LATEST version has priority of 0.9 and changefreq of daily.
+        self.assertEqual(
+            response.context['versions'][1]['loc'],
+            self.public.get_docs_url(
+                version_slug='latest',
+                lang_slug=self.public.language,
+                private=False,
+            ),)
+        self.assertEqual(response.context['versions'][1]['priority'], 0.9)
+        self.assertEqual(response.context['versions'][1]['changefreq'], 'daily')
+
     @override_settings(
         PYTHON_MEDIA=True,
         USE_SUBDOMAIN=False,