diff --git a/.isort.cfg b/.isort.cfg
index 2f6cd78cd74f366eeb3e0935312c81f2b9d25fc8..e5972edcd2f1d5696915870464e60f5d3bee1938 100644
--- a/.isort.cfg
+++ b/.isort.cfg
@@ -3,4 +3,4 @@
 line_length = 88
 multi_line_output = 3
 include_trailing_comma = true
-known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,icalevnt,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,taggit,tweepy,wagtail,wagtailmetadata,weasyprint,yaml
+known_third_party = PyPDF2,arrow,bleach,bs4,captcha,celery,dateutil,django,environ,faker,fastjsonschema,icalevnt,markdown,modelcluster,pirates,pytest,pytz,requests,sentry_sdk,taggit,tweepy,wagtail,wagtail_transfer,wagtailmetadata,weasyprint,yaml
diff --git a/Dockerfile b/Dockerfile
index 08b4f2b321dfd2eae3e6794ff3c80b6039e53f82..9a8045fdfc43b5dee7e6e59e056a82514bad6a87 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -29,6 +29,7 @@ ENV DJANGO_SETTINGS_MODULE "majak.settings.production"
 # fake values for required env variables used to run collectstatic during build
 RUN DJANGO_SECRET_KEY=x DATABASE_URL=postgres://x/x DJANGO_ALLOWED_HOSTS=x \
     OIDC_RP_CLIENT_ID=x OIDC_RP_CLIENT_SECRET=x OIDC_RP_REALM_URL=x \
+    WAGTAILTRANSFER_SECRET_KEY=x \
     python manage.py collectstatic
 
 EXPOSE 8000
diff --git a/README.md b/README.md
index 0b4ba971a430081f2c937221a5a8bc7f9142d236..259a00323c34305aa518f0c8b09eddffd7c9438f 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,7 @@ V produkci musí být navíc nastaveno:
 | `DJANGO_ALLOWED_HOSTS` | | allowed hosts (více hodnot odděleno čárkami) |
 | `CELERY_BROKER_URL` | | URL pro Celery Broker |
 | `CELERY_RESULT_BACKEND` | | URL pro Celery Result Backend |
+| `WAGTAILTRANSFER_SECRET_KEY` |  | tajný klíč pro transfer stránek |
 
 Různé:
 
@@ -157,6 +158,7 @@ Různé:
 | `SENTRY_DSN` | | pokud je zadáno, pády se reportují do Sentry |
 | `SEARCH_CONFIG` | english | nastavení jazyka fulltextového vyhledávání, viz níže |
 | `DEBUG_TOOLBAR` | False | zobrazit Django Debug Toolbar (pro vývoj) |
+| `WAGTAILTRANSFER_SOURCES` | {} | `dict` s konfigurací zdrojů pro transfer stránek |
 
 Settings pro appky na weby:
 
diff --git a/district/migrations/0103_alter_districtarticletag_content_object_and_more.py b/district/migrations/0103_alter_districtarticletag_content_object_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..411bcdebba0c5c12d040ac6a0030de010b8cf4dc
--- /dev/null
+++ b/district/migrations/0103_alter_districtarticletag_content_object_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.1.5 on 2023-01-31 19:42
+
+import django.db.models.deletion
+import modelcluster.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("district", "0102_alter_districtarticlepage_content_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="districtarticletag",
+            name="content_object",
+            field=modelcluster.fields.ParentalKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="tagged_items",
+                to="district.districtarticlepage",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="districtpersontag",
+            name="content_object",
+            field=modelcluster.fields.ParentalKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="tagged_items",
+                to="district.districtpersonpage",
+            ),
+        ),
+    ]
diff --git a/district/models.py b/district/models.py
index 608ea0e93a24002ae6b65638fa13ad1eeed5dcde..fbbc7e3c4497ee1bbeb3464551ff089748c786f0 100644
--- a/district/models.py
+++ b/district/models.py
@@ -339,7 +339,9 @@ class DistrictHomePage(
 
 class DistrictArticleTag(TaggedItemBase):
     content_object = ParentalKey(
-        "district.DistrictArticlePage", on_delete=models.CASCADE
+        "district.DistrictArticlePage",
+        on_delete=models.CASCADE,
+        related_name="tagged_items",
     )
 
 
@@ -577,7 +579,9 @@ class DistrictContactPage(
 
 class DistrictPersonTag(TaggedItemBase):
     content_object = ParentalKey(
-        "district.DistrictPersonPage", on_delete=models.CASCADE
+        "district.DistrictPersonPage",
+        on_delete=models.CASCADE,
+        related_name="tagged_items",
     )
 
 
diff --git a/elections2021/migrations/0053_alter_elections2021articletag_content_object.py b/elections2021/migrations/0053_alter_elections2021articletag_content_object.py
new file mode 100644
index 0000000000000000000000000000000000000000..3850ca8440bd1c984bdb39a162e1606840065dd4
--- /dev/null
+++ b/elections2021/migrations/0053_alter_elections2021articletag_content_object.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.5 on 2023-01-31 19:42
+
+import django.db.models.deletion
+import modelcluster.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("elections2021", "0052_alter_elections2021articlepage_content_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="elections2021articletag",
+            name="content_object",
+            field=modelcluster.fields.ParentalKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="tagged_items",
+                to="elections2021.elections2021articlepage",
+            ),
+        ),
+    ]
diff --git a/elections2021/models.py b/elections2021/models.py
index f7e2da657c7f718a0a0dd2cda74137bbf87686c6..436464bc20245c4a970bc1010fe9fee8a2a05bed 100644
--- a/elections2021/models.py
+++ b/elections2021/models.py
@@ -536,7 +536,9 @@ class Elections2021HomePage(MetadataPageMixin, RoutablePageMixin, Page):
 
 class Elections2021ArticleTag(TaggedItemBase):
     content_object = ParentalKey(
-        "elections2021.Elections2021ArticlePage", on_delete=models.CASCADE
+        "elections2021.Elections2021ArticlePage",
+        on_delete=models.CASCADE,
+        related_name="tagged_items",
     )
 
 
diff --git a/main/migrations/0043_alter_mainarticletag_content_object.py b/main/migrations/0043_alter_mainarticletag_content_object.py
new file mode 100644
index 0000000000000000000000000000000000000000..97772e76f8aeb087b87ec983b309cccd7d3436e0
--- /dev/null
+++ b/main/migrations/0043_alter_mainarticletag_content_object.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.5 on 2023-01-31 19:42
+
+import django.db.models.deletion
+import modelcluster.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("main", "0042_alter_mainarticlepage_content_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="mainarticletag",
+            name="content_object",
+            field=modelcluster.fields.ParentalKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="tagged_items",
+                to="main.mainarticlepage",
+            ),
+        ),
+    ]
diff --git a/main/models.py b/main/models.py
index 6c6803a72e5d44b009e55223d54d7c4e7c499834..642daf43ea467a0aaac63be6ef2fdf4cdeb8997e 100644
--- a/main/models.py
+++ b/main/models.py
@@ -495,7 +495,11 @@ class MainArticlesPage(
 
 
 class MainArticleTag(TaggedItemBase):
-    content_object = ParentalKey("main.MainArticlePage", on_delete=models.CASCADE)
+    content_object = ParentalKey(
+        "main.MainArticlePage",
+        on_delete=models.CASCADE,
+        related_name="tagged_items",
+    )
 
 
 class MainArticlePage(
diff --git a/majak/settings/base.py b/majak/settings/base.py
index 7672ef0c0bdcb4917e1d81f462a6360d9b569c06..bad6d65ae86347efc42feb9bd71c727f11c9758f 100644
--- a/majak/settings/base.py
+++ b/majak/settings/base.py
@@ -72,6 +72,7 @@ INSTALLED_APPS = [
     "wagtail.core",
     "wagtailmetadata",
     "wagtail_trash",
+    "wagtail_transfer",
     "modelcluster",
     "taggit",
     "django_extensions",
@@ -259,6 +260,16 @@ WAGTAILEMBEDS_RESPONSIVE_HTML = True
 BASE_URL = env.str("BASE_URL", default="https://majak.pirati.cz")
 WAGTAILADMIN_BASE_URL = BASE_URL
 
+# WAGTAIL TRANSFER SETTINGS
+# ------------------------------------------------------------------------------
+
+WAGTAILTRANSFER_SOURCES = env.json("WAGTAILTRANSFER_SOURCES", default={})
+WAGTAILTRANSFER_UPDATE_RELATED_MODELS = ["wagtailimages.Image", "wagtaildocs.Document"]
+WAGTAILTRANSFER_LOOKUP_FIELDS = {
+    "users.User": ["sso_id"],
+    "taggit.tag": ["slug"],
+}
+
 # CUSTOM SETTINGS
 # ------------------------------------------------------------------------------
 MAJAK_ENV = env.str("MAJAK_ENV", default="prod")
diff --git a/majak/settings/dev.py b/majak/settings/dev.py
index a58152a442a061e82da626cfb4b48206bc16561a..84a953fb5f03b47f107bfe593fb77fd0720a2098 100644
--- a/majak/settings/dev.py
+++ b/majak/settings/dev.py
@@ -8,6 +8,9 @@ SECRET_KEY = env("DJANGO_SECRET_KEY", default="58asda4d6nasd*jkj!dbska83asd54")
 ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])
 INSTALLED_APPS += ["wagtail.contrib.styleguide"]
 MAJAK_ENV = env.str("MAJAK_ENV", default="dev")
+WAGTAILTRANSFER_SECRET_KEY = env.str(
+    "WAGTAILTRANSFER_SECRET_KEY", default="sfdjhfssah856asjhd"
+)
 
 # django-debug-toolbar
 # ------------------------------------------------------------------------------
diff --git a/majak/settings/production.py b/majak/settings/production.py
index 33c990f1392a335cfba723961c64b16a85d55eca..36645db30b458f88d571151a222e5f66fd269e39 100644
--- a/majak/settings/production.py
+++ b/majak/settings/production.py
@@ -18,6 +18,7 @@ SECURE_HSTS_SECONDS = 518400
 SECURE_HSTS_INCLUDE_SUBDOMAINS = True
 SECURE_HSTS_PRELOAD = True
 SECURE_CONTENT_TYPE_NOSNIFF = True
+WAGTAILTRANSFER_SECRET_KEY = env.str("WAGTAILTRANSFER_SECRET_KEY")
 
 # TEMPLATES
 # ------------------------------------------------------------------------------
diff --git a/majak/urls.py b/majak/urls.py
index 7967cf7cf9d35924cc05704c6a229528abb46dba..478994bb7bc86fdf14acd02f18111ed07b23fdc1 100644
--- a/majak/urls.py
+++ b/majak/urls.py
@@ -7,6 +7,7 @@ from wagtail.admin import urls as wagtailadmin_urls
 from wagtail.contrib.sitemaps.views import sitemap
 from wagtail.core import urls as wagtail_urls
 from wagtail.documents import urls as wagtaildocs_urls
+from wagtail_transfer import urls as wagtailtransfer_urls
 
 from elections2021 import views as elections2021_views
 from maps_utils import urls as maps_utils_urls
@@ -27,6 +28,7 @@ urlpatterns = [
     path("captcha/", include(captcha.urls)),
     path("seznam-webu/", SitesListView.as_view()),
     path("sitemap.xml", sitemap),
+    path("wagtail-transfer/", include(wagtailtransfer_urls)),
 ] + pirates_urlpatterns
 
 
diff --git a/requirements/base.in b/requirements/base.in
index 0cd088a7fe2c0e5a0ed83397828e20fac0a2532f..a5ce82ca32ebce87821894b5dd24641e500a3846 100644
--- a/requirements/base.in
+++ b/requirements/base.in
@@ -1,6 +1,7 @@
 wagtail
 wagtail-metadata
 wagtail-trash
+wagtail-transfer
 django-environ
 django-extensions
 django-redis
diff --git a/requirements/base.txt b/requirements/base.txt
index e07195ba9e7ee5b4ddebb48e2b56fb8a294ecf93..211b0de4062afd42266eb4450d4c811d1a05e429 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -272,6 +272,8 @@ wagtail==4.1.1
     #   wagtail-trash
 wagtail-metadata==4.0.2
     # via -r base.in
+wagtail-transfer==0.8.5
+    # via -r base.in
 wagtail-trash==0.3.0
     # via -r base.in
 wcwidth==0.2.6
diff --git a/uniweb/migrations/0035_alter_uniwebarticletag_content_object.py b/uniweb/migrations/0035_alter_uniwebarticletag_content_object.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e63a0192388900263eef102b03f4313ce1ba580
--- /dev/null
+++ b/uniweb/migrations/0035_alter_uniwebarticletag_content_object.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.5 on 2023-01-31 19:42
+
+import django.db.models.deletion
+import modelcluster.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("uniweb", "0034_alter_uniwebarticlepage_content_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="uniwebarticletag",
+            name="content_object",
+            field=modelcluster.fields.ParentalKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="tagged_items",
+                to="uniweb.uniwebarticlepage",
+            ),
+        ),
+    ]
diff --git a/uniweb/models.py b/uniweb/models.py
index 4b0533fa2ec309d9d11176ca47c17b52e661331c..bc703145f2ea9aa4ddd51dee933b457f5ed0ae1e 100644
--- a/uniweb/models.py
+++ b/uniweb/models.py
@@ -284,7 +284,11 @@ CONTENT_STREAM_BLOCKS = [
 
 
 class UniwebArticleTag(TaggedItemBase):
-    content_object = ParentalKey("uniweb.UniwebArticlePage", on_delete=models.CASCADE)
+    content_object = ParentalKey(
+        "uniweb.UniwebArticlePage",
+        on_delete=models.CASCADE,
+        related_name="tagged_items",
+    )
 
 
 class UniwebHomePage(