Repository: wagtail/wagtail

Issue:
Add alias_of to page meta in api response
We started using aliases and needed to find a way to determine if the current page is an alias in order to set a rel=canonical header pointing to the original page.

The API currently doesn't provide a way to distinguish a normal page from an alias.

As noted in [this stackoverflow answer](https://stackoverflow.com/questions/66581694/expose-page-alias-in-wagtail-api/67107694) this can be achieved be serializing the alias_of relation.

```
from rest_framework import serializers

from wagtail.api import APIField
from wagtail.core.models import Page

class AliasPageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Page
        fields = ('id', 'full_url')

class BasePage(Page):
    class Meta:
        abstract = True

    api_meta_fields = [
        APIField('alias_of', serializer=AliasPageSerializer()),
    ]
```

Would you consider adding this to Wagtail?


Discussion:
@jaap3 Are you wanting to work on this or do you mind if I try?
@nmalacarne  Go ahead, I'm not working on it

Resolving patch:
diff --git a/wagtail/api/v2/serializers.py b/wagtail/api/v2/serializers.py
--- a/wagtail/api/v2/serializers.py
+++ b/wagtail/api/v2/serializers.py
@@ -141,6 +141,23 @@ def to_representation(self, value):
         return serializer.to_representation(value)
 
 
+class PageAliasOfField(relations.RelatedField):
+    """
+    Serializes the "alias_of" field on Page objects.
+    """
+    def get_attribute(self, instance):
+        return instance
+
+    def to_representation(self, page):
+        if page.alias_of is None:
+            return None
+        data = OrderedDict()
+        data['id'] = page.alias_of.id
+        data['full_url'] = page.alias_of.full_url
+
+        return data
+
+
 class ChildRelationField(Field):
     """
     Serializes child relations.
@@ -327,6 +344,7 @@ class PageSerializer(BaseSerializer):
     locale = PageLocaleField(read_only=True)
     html_url = PageHtmlUrlField(read_only=True)
     parent = PageParentField(read_only=True)
+    alias_of = PageAliasOfField(read_only=True)
 
     def build_relational_field(self, field_name, relation_info):
         # Find all relation fields that point to child class and make them use
diff --git a/wagtail/api/v2/views.py b/wagtail/api/v2/views.py
--- a/wagtail/api/v2/views.py
+++ b/wagtail/api/v2/views.py
@@ -391,6 +391,7 @@ class PagesAPIViewSet(BaseAPIViewSet):
         'seo_title',
         'search_description',
         'first_published_at',
+        'alias_of',
         'parent',
         'locale',
     ]


Test patch:
diff --git a/wagtail/admin/tests/api/test_pages.py b/wagtail/admin/tests/api/test_pages.py
--- a/wagtail/admin/tests/api/test_pages.py
+++ b/wagtail/admin/tests/api/test_pages.py
@@ -173,7 +173,7 @@ def test_all_fields(self):
 
         for page in content['items']:
             self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'admin_display_title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'})
-            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'seo_title', 'slug', 'parent', 'html_url', 'search_description', 'locale', 'children', 'descendants', 'ancestors', 'translations', 'status', 'latest_revision_created_at'})
+            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'seo_title', 'slug', 'parent', 'html_url', 'search_description', 'locale', 'alias_of', 'children', 'descendants', 'ancestors', 'translations', 'status', 'latest_revision_created_at'})
 
     def test_all_fields_then_remove_something(self):
         response = self.get_response(type='demosite.BlogEntryPage', fields='*,-title,-admin_display_title,-date,-seo_title,-status')
@@ -181,7 +181,7 @@ def test_all_fields_then_remove_something(self):
 
         for page in content['items']:
             self.assertEqual(set(page.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'})
-            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'slug', 'parent', 'html_url', 'search_description', 'locale', 'children', 'descendants', 'ancestors', 'translations', 'latest_revision_created_at'})
+            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'slug', 'parent', 'html_url', 'search_description', 'locale', 'alias_of', 'children', 'descendants', 'ancestors', 'translations', 'latest_revision_created_at'})
 
     def test_all_nested_fields(self):
         response = self.get_response(type='demosite.BlogEntryPage', fields='feed_image(*)')
@@ -699,7 +699,7 @@ def test_meta_ancestors(self):
     # FIELDS
 
     def test_remove_all_meta_fields(self):
-        response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-html_url,-descendants,-latest_revision_created_at,-children,-ancestors,-show_in_menus,-seo_title,-parent,-status,-search_description')
+        response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-html_url,-descendants,-latest_revision_created_at,-alias_of,-children,-ancestors,-show_in_menus,-seo_title,-parent,-status,-search_description')
         content = json.loads(response.content.decode('UTF-8'))
 
         self.assertNotIn('meta', set(content.keys()))
diff --git a/wagtail/api/v2/tests/test_pages.py b/wagtail/api/v2/tests/test_pages.py
--- a/wagtail/api/v2/tests/test_pages.py
+++ b/wagtail/api/v2/tests/test_pages.py
@@ -268,7 +268,7 @@ def test_all_fields(self):
 
         for page in content['items']:
             self.assertEqual(set(page.keys()), {'id', 'meta', 'title', 'date', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'})
-            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'seo_title', 'slug', 'html_url', 'search_description', 'locale'})
+            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'alias_of', 'seo_title', 'slug', 'html_url', 'search_description', 'locale'})
 
     def test_all_fields_then_remove_something(self):
         response = self.get_response(type='demosite.BlogEntryPage', fields='*,-title,-date,-seo_title')
@@ -276,7 +276,7 @@ def test_all_fields_then_remove_something(self):
 
         for page in content['items']:
             self.assertEqual(set(page.keys()), {'id', 'meta', 'related_links', 'tags', 'carousel_items', 'body', 'feed_image', 'feed_image_thumbnail'})
-            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'slug', 'html_url', 'search_description', 'locale'})
+            self.assertEqual(set(page['meta'].keys()), {'type', 'detail_url', 'show_in_menus', 'first_published_at', 'alias_of', 'slug', 'html_url', 'search_description', 'locale'})
 
     def test_remove_all_fields(self):
         response = self.get_response(type='demosite.BlogEntryPage', fields='_,id,type')
@@ -1032,6 +1032,7 @@ def test_field_ordering(self):
             'seo_title',
             'search_description',
             'first_published_at',
+            'alias_of',
             'parent',
         ]
         self.assertEqual(list(content['meta'].keys()), meta_field_order)
@@ -1081,7 +1082,7 @@ def test_remove_meta_fields(self):
         self.assertNotIn('html_url', set(content['meta'].keys()))
 
     def test_remove_all_meta_fields(self):
-        response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-html_url,-search_description,-show_in_menus,-parent,-seo_title')
+        response = self.get_response(16, fields='-type,-detail_url,-slug,-first_published_at,-alias_of,-html_url,-search_description,-show_in_menus,-parent,-seo_title')
         content = json.loads(response.content.decode('UTF-8'))
 
         self.assertIn('id', set(content.keys()))