diff --git a/src/core/models/events.py b/src/core/models/events.py
index 252470f0f7592d33f9f4a294e2a7107bb5e049e5..695781807e7628d1e83f8753d7fa9ae2d96af629 100644
--- a/src/core/models/events.py
+++ b/src/core/models/events.py
@@ -6,7 +6,7 @@ from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db.models import Q
-from django.urls import reverse, NoReverseMatch
+from django.urls import NoReverseMatch
 from django.utils import timezone
 from django.utils.dateparse import parse_datetime
 from django.utils.text import slugify
@@ -53,12 +53,15 @@ class EventManager(models.Manager):
         return qs.filter(is_public=True)
 
     def conference_accessible(self, conference: Conference):
-        return self.get_queryset().filter(conference=conference, is_public=True) \
+        return (
+            self.get_queryset()
+            .filter(conference=conference, is_public=True)
             .filter(
                 Q(assembly__state_assembly__in=Assembly.PUBLIC_STATES)
                 | Q(assembly__state_channel__in=Assembly.PUBLIC_STATES)
                 | Q(kind=Event.Kind.SELF_ORGANIZED)
             )
+        )
 
     def manageable_by_user(self, user: PlatformUser, conference: Conference):
         assert user is not None
@@ -83,106 +86,136 @@ class EventManager(models.Manager):
             pass
 
         # for everybody else, only show public events
-        manageable_assemblies_qs = Assembly.objects.manageable_by_user(conference=conference, user=user) \
-            .filter(members__member=user, members__can_manage_assembly=True)
-        return qs.filter(Q(kind=Event.Kind.SELF_ORGANIZED, owner=user) | Q(assembly_id__in=manageable_assemblies_qs.values_list('id', flat=True)))
+        manageable_assemblies_qs = Assembly.objects.manageable_by_user(
+            conference=conference, user=user
+        ).filter(members__member=user, members__can_manage_assembly=True)
+        return qs.filter(
+            Q(kind=Event.Kind.SELF_ORGANIZED, owner=user)
+            | Q(assembly_id__in=manageable_assemblies_qs.values_list("id", flat=True))
+        )
 
 
 class Event(TaggedItemMixin, BackendMixin, models.Model):
     class Meta:
         constraints = [
-            models.constraints.UniqueConstraint(fields=['conference', 'slug'], name='unique_event_slug'),
-            models.constraints.CheckConstraint(check=Q(kind='sos') | Q(assembly__isnull=False), name='event_has_assembly_or_is_sos'),
+            models.constraints.UniqueConstraint(
+                fields=["conference", "slug"], name="unique_event_slug"
+            ),
+            models.constraints.CheckConstraint(
+                check=Q(kind="sos") | Q(assembly__isnull=False),
+                name="event_has_assembly_or_is_sos",
+            ),
         ]
-        verbose_name = _('Event')
-        verbose_name_plural = _('Events')
+        verbose_name = _("Event")
+        verbose_name_plural = _("Events")
 
     class Kind(models.TextChoices):
-        OFFICIAL = 'official', _('Event__kind-official')  # offizielles Event
-        ASSEMBLY = 'assembly', _('Event__kind-assembly')  # von einer Assembly kuratiert
-        SELF_ORGANIZED = 'sos', _('Event__kind-sos')      # hat Einzelperson angelegt
+        OFFICIAL = "official", _("Event__kind-official")  # offizielles Event
+        ASSEMBLY = "assembly", _("Event__kind-assembly")  # von einer Assembly kuratiert
+        SELF_ORGANIZED = "sos", _("Event__kind-sos")  # hat Einzelperson angelegt
 
     objects = EventManager()
 
     id = models.UUIDField(default=uuid4, primary_key=True, editable=False)
-    conference = ConferenceReference(related_name='events')
+    conference = ConferenceReference(related_name="events")
     slug = models.SlugField(
-        max_length=150,
-        help_text=_('Event__slug__help'),
-        verbose_name=_('Event__slug'))
+        max_length=150, help_text=_("Event__slug__help"), verbose_name=_("Event__slug")
+    )
     kind = models.CharField(
         max_length=10,
         choices=Kind.choices,
         default=Kind.ASSEMBLY,
-        help_text=_('Event__kind__help'),
-        verbose_name=_('Event__kind'))
+        help_text=_("Event__kind__help"),
+        verbose_name=_("Event__kind"),
+    )
 
-    name = models.CharField(
-        max_length=200)
-    track = models.ForeignKey(ConferenceTrack, blank=True, null=True, on_delete=models.PROTECT)
-    assembly = models.ForeignKey(Assembly, related_name='events', on_delete=models.PROTECT, blank=True, null=True)
-    room = models.ForeignKey(Room, blank=True, null=True, related_name='events', on_delete=models.PROTECT)
+    name = models.CharField(max_length=200)
+    track = models.ForeignKey(
+        ConferenceTrack, blank=True, null=True, on_delete=models.PROTECT
+    )
+    assembly = models.ForeignKey(
+        Assembly, related_name="events", on_delete=models.PROTECT, blank=True, null=True
+    )
+    room = models.ForeignKey(
+        Room, blank=True, null=True, related_name="events", on_delete=models.PROTECT
+    )
 
     language = models.CharField(
-        max_length=50, blank=True, null=True,
-        help_text=_('Event__language__help'),
-        verbose_name=_('Event__language'))
+        max_length=50,
+        blank=True,
+        null=True,
+        help_text=_("Event__language__help"),
+        verbose_name=_("Event__language"),
+    )
     abstract = models.TextField(
         blank=True,
-        help_text=_('Event__abstract__help'),
-        verbose_name=_('Event__abstract'))
+        help_text=_("Event__abstract__help"),
+        verbose_name=_("Event__abstract"),
+    )
     description = models.TextField(
         blank=True,
-        help_text=_('Event__description__help'),
-        verbose_name=_('Event__description'))
+        help_text=_("Event__description__help"),
+        verbose_name=_("Event__description"),
+    )
     description_html = models.TextField(blank=True)
     banner_image_height = models.PositiveIntegerField(blank=True, null=True)
     banner_image_width = models.PositiveIntegerField(blank=True, null=True)
     banner_image = models.ImageField(
-        blank=True, null=True,
-        height_field='banner_image_height',
-        width_field='banner_image_width',
-        help_text=_('Event__banner_image__help'),
-        verbose_name=_('Event__banner_image'))
+        blank=True,
+        null=True,
+        height_field="banner_image_height",
+        width_field="banner_image_width",
+        help_text=_("Event__banner_image__help"),
+        verbose_name=_("Event__banner_image"),
+    )
 
     is_public = models.BooleanField(
         default=False,
-        help_text=_('Event__is_public__help'),
-        verbose_name=_('Event__is_public'))
+        help_text=_("Event__is_public__help"),
+        verbose_name=_("Event__is_public"),
+    )
     schedule_start = models.DateTimeField(
-        blank=True, null=True,
-        help_text=_('Event__schedule_start__help'),
-        verbose_name=_('Event__schedule_start'))
+        blank=True,
+        null=True,
+        help_text=_("Event__schedule_start__help"),
+        verbose_name=_("Event__schedule_start"),
+    )
     schedule_duration = models.DurationField(
-        blank=True, null=True,
-        help_text=_('Event__schedule_duration__help'),
-        verbose_name=_('Event__schedule_duration'))
+        blank=True,
+        null=True,
+        help_text=_("Event__schedule_duration__help"),
+        verbose_name=_("Event__schedule_duration"),
+    )
     schedule_end = models.DateTimeField(
-        blank=True, null=True,
-        help_text=_('Event__schedule_end__help'),
-        verbose_name=_('Event__schedule_end'))
+        blank=True,
+        null=True,
+        help_text=_("Event__schedule_end__help"),
+        verbose_name=_("Event__schedule_end"),
+    )
 
     additional_data = models.JSONField(blank=True, null=True)
     favorited_by = models.ManyToManyField(
         PlatformUser,
-        related_name='favorite_events',
-        help_text=_('Event__favorited_by__help'),
-        verbose_name=_('Event__favorited_by')
+        related_name="favorite_events",
+        help_text=_("Event__favorited_by__help"),
+        verbose_name=_("Event__favorited_by"),
     )
     in_personal_calendar = models.ManyToManyField(
         PlatformUser,
-        related_name='calendar_events',
-        help_text=_('Event__in_personal_calendar__help'),
-        verbose_name=_('Event__in_personal_calendar')
+        related_name="calendar_events",
+        help_text=_("Event__in_personal_calendar__help"),
+        verbose_name=_("Event__in_personal_calendar"),
     )
 
     owner = models.ForeignKey(
         settings.AUTH_USER_MODEL,
-        blank=True, null=True,
-        related_name='owned_events',
+        blank=True,
+        null=True,
+        related_name="owned_events",
         on_delete=models.PROTECT,
-        help_text=_('Event__owner__help'),
-        verbose_name=_('Event__owner'))
+        help_text=_("Event__owner__help"),
+        verbose_name=_("Event__owner"),
+    )
     """
     Ersteller/Eigentümer des Events, dies ist insb. für Self-Organized-Sessions relevant.
     """
@@ -190,8 +223,12 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
     last_update = models.DateTimeField(auto_now=True)
 
     def get_absolute_url(self):
+        from core.templatetags.hub_absolute import hub_absolute
+
         try:
-            local_url = reverse('plainui:event', kwargs={'event_slug': self.slug})
+            local_url = hub_absolute(
+                "plainui:event", event_slug=self.slug, i18n=settings.ARCHIVE_MODE
+            )
             prefix = settings.FORCE_SCRIPT_NAME or settings.SCRIPT_NAME
             if prefix is not None and local_url.startswith(prefix):
                 local_url = local_url[len(prefix):]
@@ -204,7 +241,9 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
     @property
     def public_speakers(self):
         """Returns a list of all public speakers of this event."""
-        return self.participants.objects.filter(role=EventParticipant.Role.SPEAKER, is_accepted=True, is_public=True).select_related('participant')
+        return self.participants.objects.filter(
+            role=EventParticipant.Role.SPEAKER, is_accepted=True, is_public=True
+        ).select_related("participant")
 
     def get_all_speaker_names(self):
         # plainui will prefetch speaker names in self.speakers => only do db lookup if we don't have that
@@ -212,11 +251,15 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
             names = OrderedSet(self.speakers)
         except AttributeError:
             names = OrderedSet()
-            names |= OrderedSet(self.participants.filter(is_public=True, role=EventParticipant.Role.SPEAKER).values_list('participant__username', flat=True))
+            names |= OrderedSet(
+                self.participants.filter(
+                    is_public=True, role=EventParticipant.Role.SPEAKER
+                ).values_list("participant__username", flat=True)
+            )
 
         if self.additional_data is not None:
-            for x in self.additional_data.get('persons', []):
-                names.add(x.get('public_name'))
+            for x in self.additional_data.get("persons", []):
+                names.add(x.get("public_name"))
 
         if self.kind == Event.Kind.SELF_ORGANIZED and self.owner:
             names.add(self.owner.username)
@@ -224,11 +267,11 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
         return names
 
     def get_url(self, domain=None):
-        if domain == 'media.ccc.de':
-            return f'https://media.ccc.de/v/{self.id}'
+        if domain == "media.ccc.de":
+            return f"https://media.ccc.de/v/{self.id}"
 
         if self.additional_data is not None:
-            return self.additional_data.get('url')
+            return self.additional_data.get("url")
 
         return None
 
@@ -249,9 +292,17 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
         return self.name
 
     @classmethod
-    def from_dict(cls, data: dict, conference: Conference, assembly: Assembly = None, existing=None, pop_used_keys: bool = False,
-                  allow_kind: bool = False, allow_track: bool = False,
-                  room_lookup=None):
+    def from_dict(
+        cls,
+        data: dict,
+        conference: Conference,
+        assembly: Assembly = None,
+        existing=None,
+        pop_used_keys: bool = False,
+        allow_kind: bool = False,
+        allow_track: bool = False,
+        room_lookup=None,
+    ):
         """
         Loads an Event instance from the given dictionary.
         An existing event can be provided which's data is overwritten (in parts).
@@ -268,18 +319,26 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
         :returns: a new event or the existing one with fields updated from the given data
         :rtype: Event
         """
-        assert isinstance(data, dict), 'Data must be a dictionary.'
-        assert existing is None or isinstance(existing, cls), 'Existing instance must be an Event.'
+        assert isinstance(data, dict), "Data must be a dictionary."
+        assert existing is None or isinstance(
+            existing, cls
+        ), "Existing instance must be an Event."
 
-        given_uuid = UUID(data.pop('guid')) if 'guid' in data else None
+        given_uuid = UUID(data.pop("guid")) if "guid" in data else None
         if existing:
             obj = existing
             if assembly:
-                assert obj.assembly == assembly, 'Existing event\'s assembly does not match given one.'
+                assert (
+                    obj.assembly == assembly
+                ), "Existing event's assembly does not match given one."
             if conference:
-                assert obj.conference == conference, 'Existing event\'s conference does not match given one.'
+                assert (
+                    obj.conference == conference
+                ), "Existing event's conference does not match given one."
             if given_uuid is not None:
-                assert obj.pk == given_uuid, f'expected existing event\'s id {obj.pk} to match the given uuid {given_uuid}'
+                assert (
+                    obj.pk == given_uuid
+                ), f"expected existing event's id {obj.pk} to match the given uuid {given_uuid}"
         else:
             obj = cls(assembly=assembly, conference=conference)
             if given_uuid is not None:
@@ -287,14 +346,17 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
         assert obj.conference_id is not None
 
         direct_fields = [
-            'slug', 'name',
-            'abstract', 'description',
-            'language',
-            'is_public',
-            'schedule_start', 'schedule_end',
+            "slug",
+            "name",
+            "abstract",
+            "description",
+            "language",
+            "is_public",
+            "schedule_start",
+            "schedule_end",
         ]
-        bool_fields = ['is_public']
-        dt_fields = ['schedule_start', 'schedule_end']
+        bool_fields = ["is_public"]
+        dt_fields = ["schedule_start", "schedule_end"]
         for fld in direct_fields:
             if fld in data:
                 value = data[fld] if not pop_used_keys else data.pop(fld)
@@ -308,39 +370,43 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
                 if pop_used_keys:
                     del data[fld]
 
-        if 'schedule_duration' in data:
-            value = data['schedule_duration']
-            obj.schedule_duration = str2timedelta(value) if not isinstance(value, timedelta) else value
+        if "schedule_duration" in data:
+            value = data["schedule_duration"]
+            obj.schedule_duration = (
+                str2timedelta(value) if not isinstance(value, timedelta) else value
+            )
             if pop_used_keys:
-                del data['schedule_duration']
+                del data["schedule_duration"]
 
-        if 'kind' in data:
+        if "kind" in data:
             if allow_kind:
-                obj.kind = data['kind']
+                obj.kind = data["kind"]
             if pop_used_keys:
-                del data['kind']
+                del data["kind"]
 
-        if 'track' in data:
+        if "track" in data:
             if allow_track:
-                track_q = Q(slug__iexact=data['track']) | Q(name__iexact=data['track'])
+                track_q = Q(slug__iexact=data["track"]) | Q(name__iexact=data["track"])
                 trk = assembly.conference.tracks.get(track_q)
                 obj.track = trk
             if pop_used_keys:
-                del data['track']
+                del data["track"]
 
-        if 'room' in data:
+        if "room" in data:
             if room_lookup is not None:
-                room = room_lookup(data['room'])
+                room = room_lookup(data["room"])
                 obj.room = room
             else:
-                raise RuntimeWarning('Event.from_dict() got data with "room" but no room_lookup was provided.')
+                raise RuntimeWarning(
+                    'Event.from_dict() got data with "room" but no room_lookup was provided.'
+                )
             if pop_used_keys:
-                del data['room']
+                del data["room"]
 
         if pop_used_keys:
             obj.additional_data = data
-        elif 'additional_data' in data:
-            obj.additional_data = data['additional_data']
+        elif "additional_data" in data:
+            obj.additional_data = data["additional_data"]
 
         obj.clean()
         return obj
@@ -352,47 +418,73 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
 
         # ensure track is of the same conference
         if self.track_id is not None and self.track.conference_id != self.conference_id:
-            errors['track'] = _('Event__track__needsameconference')
+            errors["track"] = _("Event__track__needsameconference")
 
         # ensure assembly is in the same conference
         if self.assembly_id and self.assembly.conference_id != self.conference_id:
-            errors['assembly'] = _('Event__assembly__needssameconference')
+            errors["assembly"] = _("Event__assembly__needssameconference")
 
         # ensure room belongs to ...
         if self.room_id is not None:
             # ... the same conference
             if self.room.conference_id != self.conference_id:
-                errors['room'] = errors.get('room', []).append(_('Event__room__needssameconference'))
+                errors["room"] = errors.get("room", []).append(
+                    _("Event__room__needssameconference")
+                )
 
             # ... the same assembly
             if self.assembly_id is None:
                 self.assembly = self.room.assembly
             elif self.room.assembly != self.assembly:
-                errors['room'] = errors.get('room', []).append(_('Event__room__needssameassembly'))
+                errors["room"] = errors.get("room", []).append(
+                    _("Event__room__needssameassembly")
+                )
 
         # official events only in an official assembly
         if self.kind == Event.Kind.OFFICIAL and not self.assembly.is_official:
-            errors['kind'] = _('Event__kind__officalassemblyonly')
+            errors["kind"] = _("Event__kind__officalassemblyonly")
 
         # start must be within conference timeframe
         if self.schedule_start is not None:
-            if (self.conference.start is not None and self.conference.start > self.schedule_start) or \
-               (self.conference.end is not None and self.conference.end < self.schedule_start):
-                errors['schedule_start'] = _('Event__schedule_start__outside_conference')
+            if (
+                self.conference.start is not None
+                and self.conference.start > self.schedule_start
+            ) or (
+                self.conference.end is not None
+                and self.conference.end < self.schedule_start
+            ):
+                errors["schedule_start"] = _(
+                    "Event__schedule_start__outside_conference"
+                )
 
         if self.schedule_duration is not None:
             # duration must not be zero
-            if self.schedule_duration == '' or self.schedule_duration == 0:
-                errors['schedule_duration'] = _('Event__schedule_duration__must_not_be_zero')
+            if self.schedule_duration == "" or self.schedule_duration == 0:
+                errors["schedule_duration"] = _(
+                    "Event__schedule_duration__must_not_be_zero"
+                )
 
             # duration cannot be longer than the whole conference
-            elif self.conference is not None and self.conference.end is not None and self.schedule_duration > (self.conference.end - self.conference.start):
-                errors['schedule_duration'] = _('Event__schedule_duration__longer_than_conference')
+            elif (
+                self.conference is not None
+                and self.conference.end is not None
+                and self.schedule_duration
+                > (self.conference.end - self.conference.start)
+            ):
+                errors["schedule_duration"] = _(
+                    "Event__schedule_duration__longer_than_conference"
+                )
 
             # end cannot be after the end of the conference
-            if self.conference is not None and self.conference.end is not None and self.schedule_start is not None and \
-               (self.schedule_start + self.schedule_duration) > self.conference.end:
-                errors['schedule_duration'] = errors.get('schedule_duration') or _('Event__schedule_end__outside_conference')
+            if (
+                self.conference is not None
+                and self.conference.end is not None
+                and self.schedule_start is not None
+                and (self.schedule_start + self.schedule_duration) > self.conference.end
+            ):
+                errors["schedule_duration"] = errors.get("schedule_duration") or _(
+                    "Event__schedule_end__outside_conference"
+                )
 
         if errors:
             raise ValidationError(errors)
@@ -407,11 +499,26 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
 
         if not self.slug:
             self.slug = gen_slug = slugify(self.name)[:50]
-            while Event.objects.filter(conference=self.conference, slug=self.slug).exclude(pk=self.pk).exists():
-                self.slug = gen_slug[:45] + '_' + ''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789') for i in range(4)])
-
-        if update_fields is None or 'description' in update_fields:
-            compile_translated_markdown_fields(self, 'description')
+            while (
+                Event.objects.filter(conference=self.conference, slug=self.slug)
+                .exclude(pk=self.pk)
+                .exists()
+            ):
+                self.slug = (
+                    gen_slug[:45]
+                    + "_"
+                    + "".join(
+                        [
+                            random.SystemRandom().choice(
+                                "abcdefghijklmnopqrstuvwxyz0123456789"
+                            )
+                            for i in range(4)
+                        ]
+                    )
+                )
+
+        if update_fields is None or "description" in update_fields:
+            compile_translated_markdown_fields(self, "description")
 
         return super().save(*args, update_fields=update_fields, **kwargs)
 
@@ -431,19 +538,19 @@ class Event(TaggedItemMixin, BackendMixin, models.Model):
 
 def _EventAttachment_upload_path(instance, filename):
     # file will be uploaded to MEDIA_ROOT/user_<id>/<filename>
-    return 'eventattachment/{0}/{1}'.format(instance.event.id, instance.id)
+    return "eventattachment/{0}/{1}".format(instance.event.id, instance.id)
 
 
 class EventAttachment(models.Model):
     class Meta:
-        verbose_name = _('EventAttachment')
-        verbose_name_plural = _('EventAttachments')
+        verbose_name = _("EventAttachment")
+        verbose_name_plural = _("EventAttachments")
 
     class Visibility(models.TextChoices):
-        PRIVATE = 'private', _('EventAttachment__visibility-private')
-        PARTICIPANTS = 'participants', _('EventAttachment__visibility-participants')
-        CONFERENCE = 'conference', _('EventAttachment__visibility-conference')
-        PUBLIC = 'public', _('EventAttachment__visibility-public')
+        PRIVATE = "private", _("EventAttachment__visibility-private")
+        PARTICIPANTS = "participants", _("EventAttachment__visibility-participants")
+        CONFERENCE = "conference", _("EventAttachment__visibility-conference")
+        PUBLIC = "public", _("EventAttachment__visibility-public")
 
     event = models.ForeignKey(Event, on_delete=models.CASCADE)
     id = models.UUIDField(default=uuid4, primary_key=True, editable=False)
@@ -452,65 +559,83 @@ class EventAttachment(models.Model):
     filename = models.CharField(max_length=255)
     mime_type = models.CharField(max_length=100)
 
-    visibility = models.CharField(max_length=20, choices=Visibility.choices, default=Visibility.PRIVATE)
+    visibility = models.CharField(
+        max_length=20, choices=Visibility.choices, default=Visibility.PRIVATE
+    )
 
 
 class EventParticipant(models.Model):
     class Role(models.TextChoices):
-        SPEAKER = 'speaker', _('EventParticipant__type-speaker')
-        ANGEL = 'angel', _('EventParticipant__type-angel')
-        REGULAR = 'regular', _('EventParticipant__type-regular')
-        PROSPECT = 'prospect', _('EventParticipant__type-prospect')
+        SPEAKER = "speaker", _("EventParticipant__type-speaker")
+        ANGEL = "angel", _("EventParticipant__type-angel")
+        REGULAR = "regular", _("EventParticipant__type-regular")
+        PROSPECT = "prospect", _("EventParticipant__type-prospect")
 
     id = models.UUIDField(default=uuid4, primary_key=True, editable=False)
 
-    event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='participants')
-    participant = models.ForeignKey(PlatformUser, on_delete=models.CASCADE, related_name='events')
+    event = models.ForeignKey(
+        Event, on_delete=models.CASCADE, related_name="participants"
+    )
+    participant = models.ForeignKey(
+        PlatformUser, on_delete=models.CASCADE, related_name="events"
+    )
 
     role = models.CharField(
-        max_length=20, choices=Role.choices, default=Role.REGULAR,
-        help_text=_('EventParticipant__role__help'),
-        verbose_name=_('EventParticipant__role'))
+        max_length=20,
+        choices=Role.choices,
+        default=Role.REGULAR,
+        help_text=_("EventParticipant__role__help"),
+        verbose_name=_("EventParticipant__role"),
+    )
 
     is_accepted = models.BooleanField(
         default=False,
-        help_text=_('EventParticipant__is_accepted__help'),
-        verbose_name=_('EventParticipant__is_accepted'))
+        help_text=_("EventParticipant__is_accepted__help"),
+        verbose_name=_("EventParticipant__is_accepted"),
+    )
     is_public = models.BooleanField(
         default=False,
-        help_text=_('EventParticipant__is_public__help'),
-        verbose_name=_('EventParticipant__is_public'))
+        help_text=_("EventParticipant__is_public__help"),
+        verbose_name=_("EventParticipant__is_public"),
+    )
 
     public_description = models.TextField(
-        blank=True, null=True,
-        help_text=_('EventParticipant__public_description__help'),
-        verbose_name=_('EventParticipant__public_description'))
+        blank=True,
+        null=True,
+        help_text=_("EventParticipant__public_description__help"),
+        verbose_name=_("EventParticipant__public_description"),
+    )
     personal_comment = models.TextField(
-        blank=True, null=True,
-        help_text=_('EventParticipant__personal_comment__help'),
-        verbose_name=_('EventParticipant__personal_comment'))
+        blank=True,
+        null=True,
+        help_text=_("EventParticipant__personal_comment__help"),
+        verbose_name=_("EventParticipant__personal_comment"),
+    )
 
     def clean(self, *args, **kwargs):
         # verify that the participant is a member of the event's conference
         try:
-            ConferenceMember.objects.get(conference=self.event.conference, user=self.participant)
+            ConferenceMember.objects.get(
+                conference=self.event.conference, user=self.participant
+            )
         except ConferenceMember.DoesNotExist:
-            raise ValidationError(_('EventParticipant__must_be_conference_member'))
+            raise ValidationError(_("EventParticipant__must_be_conference_member"))
 
     def __str__(self):
         result = self.participant.username
         if self.role in [self.Role.SPEAKER, self.Role.ANGEL]:
-            result += ' ' + self.get_role_display()
+            result += " " + self.get_role_display()
         return result
 
 
 class EventLikeCount(models.Model):
     class Meta:
-        indexes = [
-            models.Index(fields=['event1', 'like_ratio'])
-        ]
-        unique_together = [('event1', 'event2')]
-    event1 = models.ForeignKey(Event, related_name='suggestions', on_delete=models.CASCADE)
-    event2 = models.ForeignKey(Event, related_name='+', on_delete=models.CASCADE)
+        indexes = [models.Index(fields=["event1", "like_ratio"])]
+        unique_together = [("event1", "event2")]
+
+    event1 = models.ForeignKey(
+        Event, related_name="suggestions", on_delete=models.CASCADE
+    )
+    event2 = models.ForeignKey(Event, related_name="+", on_delete=models.CASCADE)
     likes = models.IntegerField()
     like_ratio = models.IntegerField()
diff --git a/src/core/templatetags/hub_absolute.py b/src/core/templatetags/hub_absolute.py
new file mode 100644
index 0000000000000000000000000000000000000000..f3e169970e7732da7c12442eb155742832d2eb08
--- /dev/null
+++ b/src/core/templatetags/hub_absolute.py
@@ -0,0 +1,99 @@
+from typing import Optional
+
+from django.conf import settings
+from django.conf.urls.i18n import i18n_patterns
+from django.contrib import admin
+from django.http import HttpRequest
+from django.template.defaulttags import register
+from django.urls import (
+    NoReverseMatch,
+    get_script_prefix,
+    include,
+    path,
+    reverse,
+    set_script_prefix,
+)
+from django.utils import translation
+
+PREFIXES = {
+    "plainui": settings.PLAINUI_BASE_URL,
+}
+
+
+class PlainUIUrlPatterns:
+    def __init__(self) -> None:
+        self.urlpatterns = i18n_patterns(
+            path("", include("plainui.urls", namespace="plainui")),
+            prefix_default_language=True,
+        )
+
+
+class UrlPatternWrapper:
+    def __init__(self, namespace, urls) -> None:
+        self.urlpatterns = [path("", include(urls, namespace=namespace))]
+
+
+URL_CONFIGS = {
+    "plain": {
+        "admin": admin.site.urls,
+        "api": UrlPatternWrapper("api", "api.urls"),
+        "backoffice": UrlPatternWrapper("backoffice", "backoffice.urls"),
+        "plainui": UrlPatternWrapper("plainui", "plainui.urls"),
+    },
+    "i18n": {
+        "admin": admin.site.urls,
+        "api": UrlPatternWrapper("api", "api.urls"),
+        "backoffice": UrlPatternWrapper("backoffice", "backoffice.urls"),
+        "plainui": PlainUIUrlPatterns(),
+    },
+}
+
+
+@register.simple_tag
+def hub_absolute(
+    url: str,
+    *args,
+    i18n: bool = True,
+    lang: Optional[str] = None,
+    query_string: str = "",
+    **kwargs,
+):
+    try:
+        app, _ = url.split(":")
+    except ValueError:
+        raise NoReverseMatch("No reverse found, missing namespace")
+    thread_prefix = get_script_prefix()
+    set_script_prefix(PREFIXES[app])
+    cur_language = translation.get_language()
+    try:
+        if lang:
+            # switch language if requested
+            translation.activate(lang)
+        reverse_url = reverse(
+            url,
+            URL_CONFIGS["i18n" if i18n else "plain"][app],
+            current_app=app,
+            args=args,
+            kwargs=kwargs,
+        )
+    finally:
+        translation.activate(cur_language)
+    set_script_prefix(thread_prefix)
+    return reverse_url if not query_string else f"{reverse_url}?{query_string}"
+
+
+@register.simple_tag
+def hub_absolute_self(
+    request: HttpRequest, i18n: bool = True, lang: Optional[str] = None
+):
+    url = f"{request.resolver_match.app_name}:{request.resolver_match.url_name}"
+    query_string = request.META.get("QUERY_STRING")
+
+    return hub_absolute(
+        url,
+        i18n=i18n,
+        lang=lang,
+        query_string=query_string,
+        *request.resolver_match.args,
+        **request.resolver_match.kwargs,
+    )
diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py
index 2e7ae67748aa09d6bdbfea89d1967a07524a35cc..821b3b7c4ae0804c9602c26345546934ab9664ee 100644
--- a/src/hub/settings/base.py
+++ b/src/hub/settings/base.py
@@ -20,56 +20,60 @@ from django.utils.translation import gettext_lazy as _
 import environ
 
 
-SCRIPT_NAME = os.getenv('SCRIPT_NAME', '')
+SCRIPT_NAME = os.getenv("SCRIPT_NAME", "")
 
 env = environ.FileAwareEnv(
     # set casting, default value
-    DJANGO_ENV_FILE=(str, ''),
-    DJANGO_DEBUG=(str, ''),
-    LOG_LEVEL=(str, 'WARN'),
-    ALLOWED_HOSTS=(list, ['127.0.0.1', 'localhost']),
-    MAIL_FROM=(str, 'webmaster+mail_from_not_configured@localhost'),
-    STATIC_URL=(str, f'{SCRIPT_NAME}/static/'),
-    STORAGE_TYPE=(str, ''),
-    MEDIA_URL=(str, f'{SCRIPT_NAME}/media/'),
+    DJANGO_ENV_FILE=(str, ""),
+    DJANGO_DEBUG=(str, ""),
+    LOG_LEVEL=(str, "WARN"),
+    ALLOWED_HOSTS=(list, ["127.0.0.1", "localhost"]),
+    MAIL_FROM=(str, "webmaster+mail_from_not_configured@localhost"),
+    STATIC_URL=(str, f"{SCRIPT_NAME}/static/"),
+    STORAGE_TYPE=(str, ""),
+    MEDIA_URL=(str, f"{SCRIPT_NAME}/media/"),
     REDIS_URL=(str, None),
-    COOKIE_NAME=(str, 'HUB_SESSION'),
-    COOKIE_PATH=(str, SCRIPT_NAME),  # defaults to empty string, '/' will be used below in this case anyhow
+    COOKIE_NAME=(str, "HUB_SESSION"),
+    COOKIE_PATH=(
+        str,
+        SCRIPT_NAME,
+    ),  # defaults to empty string, '/' will be used below in this case anyhow
     CLIENT_IP_HEADER=(str, None),
     DISABLE_RATELIMIT=(bool, False),
-
     SENTRY_ENDPOINT=(str, None),
-    SENTRY_TRACES_SAMPLE_RATE=(float, 0.05),  # create a trace for this percentage of all requests
-    SENTRY_PROFILES_SAMPLE_RATE=(float, 0.50),  # do a profiling for this percentage of _traced_ requests
-
+    SENTRY_TRACES_SAMPLE_RATE=(
+        float,
+        0.05,
+    ),  # create a trace for this percentage of all requests
+    SENTRY_PROFILES_SAMPLE_RATE=(
+        float,
+        0.50,
+    ),  # do a profiling for this percentage of _traced_ requests
     SSO_SECRET=(str, None),
     SSO_SECRET_GENERATE=(bool, False),
-    PRETIX_ISSUER=(str, 'tickets.events.ccc.de'),
+    PRETIX_ISSUER=(str, "tickets.events.ccc.de"),
     PRETIX_SECRET=(str, None),
-
-    METRICS_SERVER_IPS=(list, ['*']),
-    TIMEZONE=(str, 'Europe/Berlin'),
-
+    METRICS_SERVER_IPS=(list, ["*"]),
+    TIMEZONE=(str, "Europe/Berlin"),
+    ARCHIVE_MODE=(bool, False),
     BIGBLUEBUTTON=(bool, False),
     BIGBLUEBUTTON_API_URL=(str, None),
     BIGBLUEBUTTON_API_TOKEN=(str, None),
     BIGBLUEBUTTON_END_MEETING_CALLBACK=(str, None),
     BIGBLUEBUTTON_INITIAL_PRESENTATION_URL=(str, None),
-
     HANGAR=(bool, False),
     HANGAR_URL=(str, None),
-
     WORKADVENTURE=(bool, False),
-    WORKADVENTURE_URL_SCHEME_GENERAL=(str, 'http://visit.at.localhost:8080/'),
+    WORKADVENTURE_URL_SCHEME_GENERAL=(str, "http://visit.at.localhost:8080/"),
     WORKADVENTURE_URL_SCHEME_ASSEMBLY=(
         str,
-        'http://visit.at.localhost:8080/as/{assembly_slug}',
+        "http://visit.at.localhost:8080/as/{assembly_slug}",
     ),
     WORKADVENTURE_URL_SCHEME_REGISTER=(
         str,
-        'http://visit.at.localhost:8080/register/{token}',
+        "http://visit.at.localhost:8080/register/{token}",
     ),
-    WORKADVENTURE_CORS_ORIGIN=(str, 'https://visit.at.localhost'),
+    WORKADVENTURE_CORS_ORIGIN=(str, "https://visit.at.localhost"),
     WORKADVENTURE_BACKEND_MAP_PUSH_URL=(str, None),
     WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION=(str, None),
     WORKADVENTURE_BACKEND_USERINFO_PUSH_URL=(str, None),
@@ -83,14 +87,12 @@ env = environ.FileAwareEnv(
     WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL=(str, None),
     WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION=(str, None),
     WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY=(bool, True),
-
-    PLAINUI_BASE_URL=(str, ''),
+    PLAINUI_BASE_URL=(str, ""),
     PLAINUI_CONFERENCE=(str, None),
-    PLAINUI_DEREFERER_URL=(str, '//dereferrer/{quoted_target}'),
+    PLAINUI_DEREFERER_URL=(str, "//dereferrer/{quoted_target}"),
     PLAINUI_DEREFERER_ALLOWLIST=(list, []),
     PLAINUI_DEREFERER_COUNTLIST=(list, []),
-
-    SHIBBOLEET_WA_ROOM_ID=('str', None),
+    SHIBBOLEET_WA_ROOM_ID=("str", None),
 )
 
 
@@ -98,38 +100,49 @@ def gen_secret_key():
     """generates a value suitable for SECRET_KEY"""
     import random
 
-    SECRET_CHARSET = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
-    return ''.join([random.SystemRandom().choice(SECRET_CHARSET) for i in range(50)])
+    SECRET_CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
+    return "".join([random.SystemRandom().choice(SECRET_CHARSET) for i in range(50)])
 
 
 # regex to match arbitrary hostnames
-REGEX_HOST = re.compile(r'([\w\d-]+\.)+\w+(:\d+)?')
+REGEX_HOST = re.compile(r"([\w\d-]+\.)+\w+(:\d+)?")
 
 
 # helper function for parsing DEREFERER_*LIST (allow hostname only, regex and https:// prefixed strings)
 def _handle_hostpattern_list(target: list, items: list):
     for item in items:
-        if item.startswith('/') and item.endswith('/'):
+        if item.startswith("/") and item.endswith("/"):
             try:
-                target.append(re.compile('https://' + item[1:-1]))
+                target.append(re.compile("https://" + item[1:-1]))
             except re.error as err:
-                print('WARNING: failed to parse regex pattern "', item, "', ignoring it: ", err, sep='')
-        elif item.startswith('https://') or item.startswith('http://'):
+                print(
+                    'WARNING: failed to parse regex pattern "',
+                    item,
+                    "', ignoring it: ",
+                    err,
+                    sep="",
+                )
+        elif item.startswith("https://") or item.startswith("http://"):
             target.append(item)
         elif REGEX_HOST.fullmatch(item):
-            target.append('https://' + item)
+            target.append("https://" + item)
         else:
-            print('WARNING: failed to parse pattern "', item, "' (expected hostname, url or slash-enclosed regex), skipping it", sep='')
+            print(
+                'WARNING: failed to parse pattern "',
+                item,
+                "' (expected hostname, url or slash-enclosed regex), skipping it",
+                sep="",
+            )
 
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
 BASE_DIR = Path(__file__).resolve().parent.parent
 
-if env('DJANGO_ENV_FILE'):
-    DJANGO_ENV_FILE_PATH = Path(env('DJANGO_ENV_FILE'))
+if env("DJANGO_ENV_FILE"):
+    DJANGO_ENV_FILE_PATH = Path(env("DJANGO_ENV_FILE"))
     assert (
         DJANGO_ENV_FILE_PATH.exists()
-    ), f'DJANGO_ENV_FILE path {DJANGO_ENV_FILE_PATH} does not exist!'
+    ), f"DJANGO_ENV_FILE path {DJANGO_ENV_FILE_PATH} does not exist!"
     environ.Env.read_env(DJANGO_ENV_FILE_PATH)
 
 # prepare SECRET_KEY but must be overridden, i.e. in default.py
@@ -137,92 +150,92 @@ if env('DJANGO_ENV_FILE'):
 SECRET_KEY = None
 
 # don't run with debug turned on in production by default
-DEBUG = env('DJANGO_DEBUG') == 'I_KNOW_WHAT_I_AM_DOING'
+DEBUG = env("DJANGO_DEBUG") == "I_KNOW_WHAT_I_AM_DOING"
 
 # allowed hosts may be multiple
-ALLOWED_HOSTS = env('ALLOWED_HOSTS')
+ALLOWED_HOSTS = env("ALLOWED_HOSTS")
 
 # read application version from marker file
-with BASE_DIR.parent.joinpath('version.json').open() as version_file:
+with BASE_DIR.parent.joinpath("version.json").open() as version_file:
     APP_VERSION_INFO = json.load(version_file)
 
 # Application definition
 
 INSTALLED_APPS = [
     # Django integrated stuff
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.humanize',
-    'django.contrib.postgres',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'django.contrib.gis',
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.humanize",
+    "django.contrib.postgres",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "django.contrib.gis",
     # 3rd party extensions
-    'modeltranslation',
-    'oauth2_provider',
-    'rest_framework',
-    'rest_framework.authtoken',
+    "modeltranslation",
+    "oauth2_provider",
+    "rest_framework",
+    "rest_framework.authtoken",
     # our apps
-    'core',
-    'plainui',
+    "core",
+    "plainui",
 ]
 
 MIDDLEWARE = [
-    'core.middleware.SetRemoteAddrMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'oauth2_provider.middleware.OAuth2TokenMiddleware',
-    'hub.middleware.conference.ConferenceLocaleMiddleware',
-    'django.middleware.common.CommonMiddleware',
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'core.middleware.TimezoneMiddleware',
+    "core.middleware.SetRemoteAddrMiddleware",
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "oauth2_provider.middleware.OAuth2TokenMiddleware",
+    "hub.middleware.conference.ConferenceLocaleMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "core.middleware.TimezoneMiddleware",
     # 'django.middleware.clickjacking.XFrameOptionsMiddleware',  # TODO drüber nachdenken ob wir die brauchen (ist default an in Django)
 ]
 
-ROOT_URLCONF = 'hub.urls'
+ROOT_URLCONF = "hub.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        'DIRS': [
-            BASE_DIR / 'templates',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [
+            BASE_DIR / "templates",
         ],
-        'APP_DIRS': True,
-        'OPTIONS': {
-            'context_processors': [
-                'django.template.context_processors.debug',
-                'django.template.context_processors.request',
-                'django.contrib.auth.context_processors.auth',
-                'django.contrib.messages.context_processors.messages',
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
             ],
         },
     },
 ]
 
-WSGI_APPLICATION = 'hub.wsgi.application'
+WSGI_APPLICATION = "hub.wsgi.application"
 
 
 # Database
 # https://docs.djangoproject.com/en/3.1/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.contrib.gis.db.backends.postgis',
-        'NAME': 'hub',
+    "default": {
+        "ENGINE": "django.contrib.gis.db.backends.postgis",
+        "NAME": "hub",
     }
 }
 
 
 # Custom User model
-AUTH_USER_MODEL = 'core.PlatformUser'
+AUTH_USER_MODEL = "core.PlatformUser"
 
 
 # default auto fields (legacy stuff for corsheaders app)
-DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
 
 
 # Password validation
@@ -230,70 +243,82 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
 AUTHENTICATION_BACKENDS = (
-    'oauth2_provider.backends.OAuth2Backend',
-    'django.contrib.auth.backends.ModelBackend',
+    "oauth2_provider.backends.OAuth2Backend",
+    "django.contrib.auth.backends.ModelBackend",
 )
 
 # Session Cookie configuration
-SESSION_COOKIE_NAME = env('COOKIE_NAME')
+SESSION_COOKIE_NAME = env("COOKIE_NAME")
 SESSION_COOKIE_HTTPONLY = True  # session cookie is unavailable to JavaScript (default)
-SESSION_COOKIE_SAMESITE = 'Lax'  # set SameSite=Lax (default)
-SESSION_COOKIE_PATH = env('COOKIE_PATH') or '/'  # use configured path, SESSION_NAME or default '/'
+SESSION_COOKIE_SAMESITE = "Lax"  # set SameSite=Lax (default)
+SESSION_COOKIE_PATH = (
+    env("COOKIE_PATH") or "/"
+)  # use configured path, SESSION_NAME or default '/'
 SESSION_COOKIE_SECURE = True  # mark session cookie as https-only
 SESSION_SAVE_EVERY_REQUEST = (
     False  # no need to update a session on each request (default)
 )
 
 # CSRF Cookie configuration
-CSRF_COOKIE_NAME = env('CSRF_COOKIE_NAME', default=SESSION_COOKIE_NAME.replace('_SESSION', '_CSRF') if '_SESSION' in SESSION_COOKIE_NAME else 'HUB_CSRF')
+CSRF_COOKIE_NAME = env(
+    "CSRF_COOKIE_NAME",
+    default=SESSION_COOKIE_NAME.replace("_SESSION", "_CSRF")
+    if "_SESSION" in SESSION_COOKIE_NAME
+    else "HUB_CSRF",
+)
 CSRF_COOKIE_PATH = SESSION_COOKIE_PATH
 CSRF_COOKIE_SECURE = SESSION_COOKIE_SECURE
 
 # OAuth2 configuration
-OAUTH2_PROVIDER_APPLICATION_MODEL = 'core.Application'
+OAUTH2_PROVIDER_APPLICATION_MODEL = "core.Application"
 OAUTH2_PROVIDER = {
-    'ALLOWED_REDIRECT_URI_SCHEMES': ['https'],
-    'SCOPES_BACKEND_CLASS': 'core.sso.OAuth2Scopes',
+    "ALLOWED_REDIRECT_URI_SCHEMES": ["https"],
+    "SCOPES_BACKEND_CLASS": "core.sso.OAuth2Scopes",
 }
 
 # Internationalization
 # https://docs.djangoproject.com/en/3.1/topics/i18n/
 
-LANGUAGE_CODE = 'en-us'
-LANGUAGE_COOKIE_NAME = env('LANGUAGE_COOKIE_NAME', default=SESSION_COOKIE_NAME.replace('_SESSION', '_LANG') if '_SESSION' in SESSION_COOKIE_NAME else 'HUB_LANG')  # noqa:E501
+LANGUAGE_CODE = "en-us"
+LANGUAGE_COOKIE_NAME = env(
+    "LANGUAGE_COOKIE_NAME",
+    default=SESSION_COOKIE_NAME.replace("_SESSION", "_LANG")
+    if "_SESSION" in SESSION_COOKIE_NAME
+    else "HUB_LANG",
+)  # noqa:E501
 LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH
 LANGUAGE_COOKIE_SECURE = SESSION_COOKIE_SECURE
 
-TIME_ZONE = env('TIMEZONE')
+TIME_ZONE = env("TIMEZONE")
 USE_I18N = True
 USE_L10N = True
 USE_TZ = True
 LANGUAGES = [
-    ('de', _("German")),
-    ('en', _("English")),
+    ("de", _("German")),
+    ("en", _("English")),
 ]
 
-MODELTRANSLATION_FALLBACK_LANGUAGES = ['en', 'de']
+MODELTRANSLATION_FALLBACK_LANGUAGES = ["en", "de"]
 
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/3.1/howto/static-files/
 
-STATIC_URL = env('STATIC_URL')
-STATIC_ROOT = BASE_DIR.parent / 'static.dist'
+STATIC_URL = env("STATIC_URL")
+STATIC_ROOT = BASE_DIR.parent / "static.dist"
 # STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
 # STATICFILES_DIRS = [
 #     BASE_DIR / "static",
@@ -306,84 +331,90 @@ STORAGES = {
     },
 }
 
-storage_type = env('STORAGE_TYPE').lower()
-if storage_type == 's3':
-    STORAGES['default'] = {'BACKEND': 'storages.backends.s3boto3.S3Boto3Storage'}
+storage_type = env("STORAGE_TYPE").lower()
+if storage_type == "s3":
+    STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
 
-    AWS_STORAGE_BUCKET_NAME = env.str('S3_BUCKET', 'static')
+    AWS_STORAGE_BUCKET_NAME = env.str("S3_BUCKET", "static")
 
-    AWS_ACCESS_KEY_ID = env.str('S3_ACCESS_KEY')
-    AWS_SECRET_ACCESS_KEY = env.str('S3_SECRET_KEY')
-    AWS_S3_ENDPOINT_URL = env.str('S3_ENDPOINT')
+    AWS_ACCESS_KEY_ID = env.str("S3_ACCESS_KEY")
+    AWS_SECRET_ACCESS_KEY = env.str("S3_SECRET_KEY")
+    AWS_S3_ENDPOINT_URL = env.str("S3_ENDPOINT")
 
     # allow a custom domain to serve the media files, the used path must _not_ end with a slash
-    AWS_S3_CUSTOM_DOMAIN = env.url('MEDIA_URL', default=AWS_S3_ENDPOINT_URL).netloc
+    AWS_S3_CUSTOM_DOMAIN = env.url("MEDIA_URL", default=AWS_S3_ENDPOINT_URL).netloc
 
-elif storage_type == 'sftp':
-    STORAGES['default'] = {'BACKEND': 'storages.backend.sftpstorage.SFTPStorage'}
-    SFTP_STORAGE_HOST = env.str('SFTP_HOST')
-    SFTP_STORAGE_ROOT = env.str('SFTP_DIR')
+elif storage_type == "sftp":
+    STORAGES["default"] = {"BACKEND": "storages.backend.sftpstorage.SFTPStorage"}
+    SFTP_STORAGE_HOST = env.str("SFTP_HOST")
+    SFTP_STORAGE_ROOT = env.str("SFTP_DIR")
     SFTP_STORAGE_INTERACTIVE = False
-    SFTP_STORAGE_PARAMS = env.dict('SFTP_PARAMS')
+    SFTP_STORAGE_PARAMS = env.dict("SFTP_PARAMS")
 
-elif storage_type == 'local' or storage_type == '':
-    if storage_type == '':
+elif storage_type == "local" or storage_type == "":
+    if storage_type == "":
         import warnings
-        warnings.warn('No STORAGE_TYPE selected, defaulting to "local". Verify if that is what you want!', RuntimeWarning)
 
-    STORAGES['default'] = {
+        warnings.warn(
+            'No STORAGE_TYPE selected, defaulting to "local". Verify if that is what you want!',
+            RuntimeWarning,
+        )
+
+    STORAGES["default"] = {
         "BACKEND": "django.core.files.storage.FileSystemStorage",
-        'OPTIONS': {
-            'base_url': env('MEDIA_URL'),
-            'location': env.path('MEDIA_PATH', BASE_DIR.parent / 'media'),
-        }
+        "OPTIONS": {
+            "base_url": env("MEDIA_URL"),
+            "location": env.path("MEDIA_PATH", BASE_DIR.parent / "media"),
+        },
     }
 
 else:
-    raise ValueError(f'Please specify storage type (s3/sftp/local), "{storage_type}" is unknown.')
+    raise ValueError(
+        f'Please specify storage type (s3/sftp/local), "{storage_type}" is unknown.'
+    )
 
 # Media (uploaded files)
-MEDIA_URL = env('MEDIA_URL')
-MEDIA_ROOT = BASE_DIR.parent / 'media'
+MEDIA_URL = env("MEDIA_URL")
+MEDIA_ROOT = BASE_DIR.parent / "media"
 
 # serve media files in MEDIA_ROOT at /media/ (ignoring configured MEDIA_URL, discouraged in production!)
 FORCE_SERVE_MEDIA = False
 
 # REST Framework Config
 REST_FRAMEWORK = {
-    'DEFAULT_AUTHENTICATION_CLASSES': [
-        'rest_framework.authentication.SessionAuthentication',
-        'rest_framework.authentication.TokenAuthentication',
+    "DEFAULT_AUTHENTICATION_CLASSES": [
+        "rest_framework.authentication.SessionAuthentication",
+        "rest_framework.authentication.TokenAuthentication",
     ]
 }
 
 # Cache
-if env('REDIS_URL'):
+if env("REDIS_URL"):
     CACHES = {
-        'default': {
-            'BACKEND': 'django_redis.cache.RedisCache',
-            'LOCATION': env('REDIS_URL'),
-            'OPTIONS': {
-                'CLIENT_CLASS': 'django_redis.client.DefaultClient',
-                'SERIALIZER': 'django_redis.serializers.json.JSONSerializer',
+        "default": {
+            "BACKEND": "django_redis.cache.RedisCache",
+            "LOCATION": env("REDIS_URL"),
+            "OPTIONS": {
+                "CLIENT_CLASS": "django_redis.client.DefaultClient",
+                "SERIALIZER": "django_redis.serializers.json.JSONSerializer",
             },
         }
     }
 else:
     CACHES = {
-        'default': {
-            'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+        "default": {
+            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
         }
     }
 
 # Header to look up real client IP in. Used for logging / rate limiting purposes. `None` = don't lookup, looked up in `request.META`
 # see https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.HttpRequest.META for details
-CLIENT_IP_HEADER = env('CLIENT_IP_HEADER')
-RATELIMIT_ENABLE = not env('DISABLE_RATELIMIT')
+CLIENT_IP_HEADER = env("CLIENT_IP_HEADER")
+RATELIMIT_ENABLE = not env("DISABLE_RATELIMIT")
 
 
 # List of disallowed assembly slugs
-FORBIDDEN_ASSEMBLY_SLUGS = ['admin', 'visit', 'maps', 'api', 'pusher']
+FORBIDDEN_ASSEMBLY_SLUGS = ["admin", "visit", "maps", "api", "pusher"]
 
 # Logging configuration
 LOGGING = {
@@ -396,22 +427,22 @@ LOGGING = {
     },
     "root": {
         "handlers": ["console"],
-        "level": env('LOG_LEVEL'),
+        "level": env("LOG_LEVEL"),
     },
 }
 
 # Mail configuration
-DEFAULT_FROM_EMAIL = env.str('MAIL_FROM')
+DEFAULT_FROM_EMAIL = env.str("MAIL_FROM")
 SERVER_EMAIL = DEFAULT_FROM_EMAIL
 MAIL_REPLY_TO = []
-MAIL_SUBJECT_PREFIX = env.str('MAIL_SUBJECT_PREFIX', default='')
-MAIL_SIGNATURE = env.str('MAIL_SIGNATURE', default='')
+MAIL_SUBJECT_PREFIX = env.str("MAIL_SUBJECT_PREFIX", default="")
+MAIL_SIGNATURE = env.str("MAIL_SIGNATURE", default="")
 SUPPORT_HTML_MAILS = False
 
-ADMINS = getaddresses([env('ADMINS', default='')])
+ADMINS = getaddresses([env("ADMINS", default="")])
 
 # try to parse mail config as URL, e.g. smtp+tls://user%40gmail.com:password@mx.local:587
-mail_config_str = env.str('MAIL_CONFIG', default=None)
+mail_config_str = env.str("MAIL_CONFIG", default=None)
 if mail_config_str:
     mail_config = environ.FileAwareEnv.email_url_config(mail_config_str)
     # update local vars with the parsed EMAIL_HOST, EMAIL_HOST_USER, etc.
@@ -421,25 +452,25 @@ if mail_config_str:
 HEADERS_DEBUG_ENDPOINT_TOKEN = None
 
 # Sentry
-SENTRY_ENDPOINT = env('SENTRY_ENDPOINT')
-SENTRY_TRACES_SAMPLE_RATE = env('SENTRY_TRACES_SAMPLE_RATE')
-SENTRY_PROFILES_SAMPLE_RATE = env('SENTRY_PROFILES_SAMPLE_RATE')
+SENTRY_ENDPOINT = env("SENTRY_ENDPOINT")
+SENTRY_TRACES_SAMPLE_RATE = env("SENTRY_TRACES_SAMPLE_RATE")
+SENTRY_PROFILES_SAMPLE_RATE = env("SENTRY_PROFILES_SAMPLE_RATE")
 
 # API access
 API_USERS = []
 
 # SSO
-SSO_COOKIE_NAME = 'SSO_TOKEN'
-SSO_COOKIE_DOMAIN = '.localhost'
-SSO_HEADER = 'X-SSO-TOKEN'
+SSO_COOKIE_NAME = "SSO_TOKEN"
+SSO_COOKIE_DOMAIN = ".localhost"
+SSO_HEADER = "X-SSO-TOKEN"
 
-SSO_SECRET = env('SSO_SECRET')
-if SSO_SECRET is None and env('SSO_SECRET_GENERATE'):
+SSO_SECRET = env("SSO_SECRET")
+if SSO_SECRET is None and env("SSO_SECRET_GENERATE"):
     SSO_SECRET = gen_secret_key()
 
 # Pretix Token integration, see core.models.ticket.ConferenceMemberTicket.validate_pretix_ticket and plainui.views.RedeemToken
-PRETIX_ISSUER = env('PRETIX_ISSUER')  # expected value in the 'iss' field of pretix
-PRETIX_SECRET_KEY = env('PRETIX_SECRET')  # the JWT shared secret with Pretix
+PRETIX_ISSUER = env("PRETIX_ISSUER")  # expected value in the 'iss' field of pretix
+PRETIX_SECRET_KEY = env("PRETIX_SECRET")  # the JWT shared secret with Pretix
 
 # ----------------------------------
 # Metrics
@@ -449,7 +480,7 @@ PRETIX_SECRET_KEY = env('PRETIX_SECRET')  # the JWT shared secret with Pretix
 # You can add one or multiple IP addresse by setting the `METRICS_SERVER_IPS` env var.
 # e.g. METRICS_SERVER_IPS="127.0.0.1,80.147.140.51"
 # The default is to allow every IP address (METRICS_SERVER_IPS="*").
-METRICS_SERVER_IPS = env('METRICS_SERVER_IPS')
+METRICS_SERVER_IPS = env("METRICS_SERVER_IPS")
 
 
 # ----------------------------------
@@ -457,8 +488,8 @@ METRICS_SERVER_IPS = env('METRICS_SERVER_IPS')
 # ----------------------------------
 
 SCHEDULE_SUPPORT = [
-    'core.schedules.schedulexml.ScheduleXMLSupport',
-    'core.schedules.schedulejson.ScheduleJSONSupport',
+    "core.schedules.schedulexml.ScheduleXMLSupport",
+    "core.schedules.schedulejson.ScheduleJSONSupport",
 ]
 
 # the file:// protocol should only be used in the unit tests and _not_ in production!
@@ -469,66 +500,66 @@ SCHEDULES_SUPPORT_FILE_PROTOCOL = False
 # ----------------------------------
 
 # BigBlueButton
-INTEGRATIONS_BBB = env('BIGBLUEBUTTON')
-BIGBLUEBUTTON_API_URL = env('BIGBLUEBUTTON_API_URL')
-BIGBLUEBUTTON_API_TOKEN = env('BIGBLUEBUTTON_API_TOKEN')
+INTEGRATIONS_BBB = env("BIGBLUEBUTTON")
+BIGBLUEBUTTON_API_URL = env("BIGBLUEBUTTON_API_URL")
+BIGBLUEBUTTON_API_TOKEN = env("BIGBLUEBUTTON_API_TOKEN")
 BIGBLUEBUTTON_END_MEETING_CALLBACK = env(
-    'BIGBLUEBUTTON_END_MEETING_CALLBACK'
+    "BIGBLUEBUTTON_END_MEETING_CALLBACK"
 )  # url bbb will call to notify us of ending meetings
 # BIGBLUEBUTTON_END_MEETING_CALLBACK = 'https://rc3.world/api/bbb_meeting_end'
 BIGBLUEBUTTON_INITIAL_PRESENTATION_URL = env(
-    'BIGBLUEBUTTON_INITIAL_PRESENTATION_URL'
+    "BIGBLUEBUTTON_INITIAL_PRESENTATION_URL"
 )  # url to tell bbb to use as initial presentation
 # BIGBLUEBUTTON_INITIAL_PRESENTATION_URL = 'https://rc3.world/static/plainui/bbb-background.jpg'
 
 # Hangar
-INTEGRATIONS_HANGAR = env('HANGAR')
-HANGAR_URL = env('HANGAR_URL')
+INTEGRATIONS_HANGAR = env("HANGAR")
+HANGAR_URL = env("HANGAR_URL")
 
 # Workadventure
-INTEGRATIONS_WORKADVENTURE = env('WORKADVENTURE')
-WORKADVENTURE_URL_SCHEME_GENERAL = env('WORKADVENTURE_URL_SCHEME_GENERAL')
-WORKADVENTURE_URL_SCHEME_ASSEMBLY = env('WORKADVENTURE_URL_SCHEME_ASSEMBLY')
-WORKADVENTURE_URL_SCHEME_REGISTER = env('WORKADVENTURE_URL_SCHEME_REGISTER')
-WORKADVENTURE_CORS_ORIGIN = env('WORKADVENTURE_CORS_ORIGIN')
+INTEGRATIONS_WORKADVENTURE = env("WORKADVENTURE")
+WORKADVENTURE_URL_SCHEME_GENERAL = env("WORKADVENTURE_URL_SCHEME_GENERAL")
+WORKADVENTURE_URL_SCHEME_ASSEMBLY = env("WORKADVENTURE_URL_SCHEME_ASSEMBLY")
+WORKADVENTURE_URL_SCHEME_REGISTER = env("WORKADVENTURE_URL_SCHEME_REGISTER")
+WORKADVENTURE_CORS_ORIGIN = env("WORKADVENTURE_CORS_ORIGIN")
 
 WORKADVENTURE_TERMINATE_OLD_SESSIONS_ON_REGISTER = False
 
 # URL (and optional Authorization-Header value) for pushing maps to exneuland, accepts "%CONFSLUG% variable
-WORKADVENTURE_BACKEND_MAP_PUSH_URL = env('WORKADVENTURE_BACKEND_MAP_PUSH_URL')
+WORKADVENTURE_BACKEND_MAP_PUSH_URL = env("WORKADVENTURE_BACKEND_MAP_PUSH_URL")
 WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION = env(
-    'WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION'
+    "WORKADVENTURE_BACKEND_MAP_PUSH_AUTHORIZATION"
 )
 
 # URL (and optional Authorization-Header value) for pushing userinfo to exneuland, accepts "%CONFSLUG% and %USERID% variables
-WORKADVENTURE_BACKEND_USERINFO_PUSH_URL = env('WORKADVENTURE_BACKEND_USERINFO_PUSH_URL')
+WORKADVENTURE_BACKEND_USERINFO_PUSH_URL = env("WORKADVENTURE_BACKEND_USERINFO_PUSH_URL")
 WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION = env(
-    'WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION'
+    "WORKADVENTURE_BACKEND_USERINFO_PUSH_AUTHORIZATION"
 )
 
 # URL (and optional Authorization-Header value) for requesting mapservice sync, accepts "%CONFSLUG% variable
-WORKADVENTURE_MAPSERVICE_SYNC_URL = env('WORKADVENTURE_MAPSERVICE_SYNC_URL')
+WORKADVENTURE_MAPSERVICE_SYNC_URL = env("WORKADVENTURE_MAPSERVICE_SYNC_URL")
 WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION = env(
-    'WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION'
+    "WORKADVENTURE_MAPSERVICE_SYNC_AUTHORIZATION"
 )
-WORKADVENTURE_MAPSERVICE_SYNC_NOVERIFY = env('WORKADVENTURE_MAPSERVICE_SYNC_NOVERIFY')
+WORKADVENTURE_MAPSERVICE_SYNC_NOVERIFY = env("WORKADVENTURE_MAPSERVICE_SYNC_NOVERIFY")
 
 # URL (and optional Authorization-Header value) for requesting mapservice sync of a single room, accepts "%CONFSLUG% and %ROOMID% variable
-WORKADVENTURE_MAPSERVICE_SYNCROOM_URL = env('WORKADVENTURE_MAPSERVICE_SYNCROOM_URL')
+WORKADVENTURE_MAPSERVICE_SYNCROOM_URL = env("WORKADVENTURE_MAPSERVICE_SYNCROOM_URL")
 WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION = env(
-    'WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION'
+    "WORKADVENTURE_MAPSERVICE_SYNCROOM_AUTHORIZATION"
 )
 WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY = env(
-    'WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY'
+    "WORKADVENTURE_MAPSERVICE_SYNCROOM_NOVERIFY"
 )
 
 # URL (and optional Authorization-Header value) for setting mapservice's loglevel, accepts "%CONFSLUG% variable
-WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL = env('WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL')
+WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL = env("WORKADVENTURE_MAPSERVICE_LOGLEVEL_URL")
 WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION = env(
-    'WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION'
+    "WORKADVENTURE_MAPSERVICE_LOGLEVEL_AUTHORIZATION"
 )
 WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY = env(
-    'WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY'
+    "WORKADVENTURE_MAPSERVICE_LOGLEVEL_NOVERIFY"
 )
 
 # push maps to backend upon new data from mapservice?
@@ -539,35 +570,39 @@ WORKADVENTURE_BACKEND_PUSH_ON_MAPSERVICE_DATA = True
 # ----------------------------------
 
 # base domain of the frontend (used to generate links in API and BackOffice, not in the plain UI itself)
-PLAINUI_BASE_URL = env('PLAINUI_BASE_URL')
+PLAINUI_BASE_URL = env("PLAINUI_BASE_URL")
 
 # Database ID of the active conference for the PlainUI.
 # To initialize create a conference in the backend, then configure the conference, then restart the UI workers
-PLAINUI_CONFERENCE = env('PLAINUI_CONFERENCE')
+PLAINUI_CONFERENCE = env("PLAINUI_CONFERENCE")
+# archive mode disables user login etc.
+ARCHIVE_MODE = env("ARCHIVE_MODE")
 
 # Absolute url to use when dereferring links. Replaces `{quoted_target}` with the urlquoted target url
 # If you modify this on a non-empty database you should run `manage.py rerender_markdown` to update cashed markdown that contains old urls.
-PLAINUI_DEREFERER_URL = env('PLAINUI_DEREFERER_URL')
+PLAINUI_DEREFERER_URL = env("PLAINUI_DEREFERER_URL")
 
 # URI patterns that are auto-allowed (never get dereferrer'd)
 # Only looks at schema and host/port, so for example for the url https://rc3.world:4321/some/where/ the entry should be `'https://rc3.world'`.
 # Can also be a regex, for example re.compile(r'https://.*\.rc3\.world')
 # DON'T ADD TRAILING SLASHES
 DEREFERRER_GLOBAL_ALLOWLIST = []
-_handle_hostpattern_list(DEREFERRER_GLOBAL_ALLOWLIST, env('PLAINUI_DEREFERER_ALLOWLIST'))
+_handle_hostpattern_list(
+    DEREFERRER_GLOBAL_ALLOWLIST, env("PLAINUI_DEREFERER_ALLOWLIST")
+)
 
 # URI patterns where following outgoing links is counted in the database
 # rules are the same as with DEREFERRER_GLOBAL_ALLOWLIST
 DEREFERRER_COUNT_ACCESS = []
-_handle_hostpattern_list(DEREFERRER_COUNT_ACCESS, env('PLAINUI_DEREFERER_COUNTLIST'))
+_handle_hostpattern_list(DEREFERRER_COUNT_ACCESS, env("PLAINUI_DEREFERER_COUNTLIST"))
 
 # Shibboleet Mode: an entrypoint /shibboleet which adds the registered user to the conference and redirects to a new WA session
 # ID of the WorkAdventure room which shall be used as the entrypoint in the new WA session
-SHIBBOLEET_WA_ROOM_ID = env('SHIBBOLEET_WA_ROOM_ID')
+SHIBBOLEET_WA_ROOM_ID = env("SHIBBOLEET_WA_ROOM_ID")
 
 
 # Slug of the Lobby Room. Default Room to join in the Workadventure
-WORKADVENTURE_LOBBY_ROOM_SLUG = 'lobby'
+WORKADVENTURE_LOBBY_ROOM_SLUG = "lobby"
 
 
 # Protected namespaces for static pages.
@@ -577,11 +612,14 @@ WORKADVENTURE_LOBBY_ROOM_SLUG = 'lobby'
 # To grant access for multiple page groups, create multiple entries for the same namespace.
 STATIC_PAGE_NAMESPACES = (
     # ('<Namespace-Prefix>', '<Required Static Pages-Group>'),
-    ('_', 'conference_organizers'),  # all pages starting with underscore (includes for other pages)
-    ('report_content', 'conference_organizers'),
-    ('start', 'conference_organizers'),
-    ('wa_contact', 'conference_organizers'),
+    (
+        "_",
+        "conference_organizers",
+    ),  # all pages starting with underscore (includes for other pages)
+    ("report_content", "conference_organizers"),
+    ("start", "conference_organizers"),
+    ("wa_contact", "conference_organizers"),
 )
 
 # flag if newly created pages are localized by default (or not)
-STATIC_PAGE_LOCALIZED_BY_DEFAULT = env.bool('STATICPAGES_LOCALIZED_BY_DEFAULT', False)
+STATIC_PAGE_LOCALIZED_BY_DEFAULT = env.bool("STATICPAGES_LOCALIZED_BY_DEFAULT", False)
diff --git a/src/plainui/jinja2.py b/src/plainui/jinja2.py
index 760b2b6f03ca53d5ee56d3bbc90ce27f593e7427..90ef5612c08758f87131a6c16961a46a6f5df4f0 100644
--- a/src/plainui/jinja2.py
+++ b/src/plainui/jinja2.py
@@ -1,4 +1,7 @@
 from datetime import datetime, timedelta
+
+
+from django.conf import settings
 from django.contrib.messages import get_messages
 from django.templatetags.static import static
 from django.urls import reverse
@@ -17,6 +20,7 @@ from modeltranslation.fields import build_localized_fieldname
 from modeltranslation.settings import AVAILABLE_LANGUAGES
 
 from core.models import PlatformUser, UserBadge
+from core.templatetags.hub_absolute import hub_absolute, hub_absolute_self
 
 
 def url(name, *args, current_app=None, **kwargs):
@@ -24,11 +28,11 @@ def url(name, *args, current_app=None, **kwargs):
 
 
 def browser_type(request):
-    user_agent = request.META.get('HTTP_USER_AGENT', '')
-    if 'MSIE' in user_agent or 'Trident/7.0;' in user_agent:
-        return 'ie'
-    if 'CERN-NextStep-WorldWideWeb' in user_agent:
-        return 'historic'
+    user_agent = request.META.get("HTTP_USER_AGENT", "")
+    if "MSIE" in user_agent or "Trident/7.0;" in user_agent:
+        return "ie"
+    if "CERN-NextStep-WorldWideWeb" in user_agent:
+        return "historic"
     return user_agent
 
 
@@ -43,7 +47,11 @@ def num_of_pending_badges(request):
     user = request.user
     if not user.is_authenticated:
         return 0
-    return len(UserBadge.objects.filter(user=user, accepted_by_user=False).select_related('badge'))
+    return len(
+        UserBadge.objects.filter(user=user, accepted_by_user=False).select_related(
+            "badge"
+        )
+    )
 
 
 def custom_timedelta(tdelta):
@@ -52,51 +60,51 @@ def custom_timedelta(tdelta):
 
 def custom_timedelta_short(tdelta: timedelta):
     if not isinstance(tdelta, timedelta):
-        return ''
+        return ""
     s = tdelta.seconds
     h = s // 3600
     s -= h * 3600
     m = s // 60
     s -= m * 60
-    return '%02d:%02d:%02d' % (h, m, s)
+    return "%02d:%02d:%02d" % (h, m, s)
 
 
 def custom_strftime(date):
     if not isinstance(date, datetime):
-        return ''
+        return ""
 
     return localize(localtime(date))
 
 
 def custom_strftimehm(date):
     if not isinstance(date, datetime):
-        return ''
+        return ""
 
-    return localtime(date).strftime('%H:%M')
+    return localtime(date).strftime("%H:%M")
 
 
 def custom_strfdate(date):
     if not isinstance(date, datetime):
-        return ''
+        return ""
 
     return localize(localdate(date))
 
 
 def custom_strfdates(date):
     if not isinstance(date, datetime):
-        return ''
+        return ""
 
-    if get_language() == 'de':
-        return localtime(date).strftime('%d.%m.%Y')
+    if get_language() == "de":
+        return localtime(date).strftime("%d.%m.%Y")
 
-    return localdate(date).strftime('%x')
+    return localdate(date).strftime("%x")
 
 
 def custom_weekday_abbrev(date):
     if not isinstance(date, datetime):
-        return ''
+        return ""
 
-    return date.strftime('%a')
+    return date.strftime("%a")
 
 
 # set up an internal represenative for an unset variable as parameter for show_vars()
@@ -113,12 +121,12 @@ def show_vars(ctx, var=_UNSET):
         var = var._wrapped
 
     try:
-        if not callable(getattr(var, 'items', None)):
+        if not callable(getattr(var, "items", None)):
             var = var.__dict__
 
-        ret = ''
-        for (k, v) in var.items():
-            ret += '%r: %r\n' % (k, v)
+        ret = ""
+        for k, v in var.items():
+            ret += "%r: %r\n" % (k, v)
         return ret
 
     except (AttributeError, TypeError):
@@ -127,24 +135,24 @@ def show_vars(ctx, var=_UNSET):
 
 @pass_context
 def css_scope(ctx):
-    request = ctx['request']
+    request = ctx["request"]
 
     if request.user.is_authenticated and request.user.high_contrast:
-        return 'hub-high-contrast'
-    scope = ctx.get('scope')
-    if scope == 'assembly':
-        return 'hub-assembly'
-    elif scope == 'world':
-        return 'hub-world'
-    return 'hub'
+        return "hub-high-contrast"
+    scope = ctx.get("scope")
+    if scope == "assembly":
+        return "hub-assembly"
+    elif scope == "world":
+        return "hub-world"
+    return "hub"
 
 
 @pass_context
 def active_theme(ctx):
-    request = ctx['request']
-    if 'theme_override' in ctx:
-        return ctx['theme_override']
-    theme = request.session.get('theme', PlatformUser.Theme.DEFAULT)
+    request = ctx["request"]
+    if "theme_override" in ctx:
+        return ctx["theme_override"]
+    theme = request.session.get("theme", PlatformUser.Theme.DEFAULT)
     return theme
 
 
@@ -167,7 +175,7 @@ class MyContext(Context):
         self.next_id = 0
 
     def get_next_id(self):
-        res = 'id_' + str(self.next_id)
+        res = "id_" + str(self.next_id)
         self.next_id += 1
         return res
 
@@ -177,33 +185,37 @@ class MyEnvironment(Environment):
 
 
 def environment(**options):
-    env = MyEnvironment(**{
-        'extensions': ["jinja2.ext.i18n", "jinja2.ext.debug"],
-        **options
-    })
-    env.globals.update({
-        'browser_type': browser_type,
-        'css_scope': css_scope,
-        'active_theme': active_theme,
-        'get_language': get_language,
-        'get_messages': get_messages,
-        'json_script': json_script,
-        'num_of_unread_messages': num_of_unread_messages,
-        'num_of_pending_badges': num_of_pending_badges,
-        'static': static,
-        'url': url,
-        'show_vars': show_vars,
-        'unique_id': unique_id,
-        'translated_fields_for_field': translated_fields_for_field,
-        'field_translation_languages': field_translation_languages,
-        'now': timezone.now(),
-    })
-    env.filters['strftdelta'] = custom_timedelta
-    env.filters['strftdelta_short'] = custom_timedelta_short
-    env.filters['strftimehm'] = custom_strftimehm
-    env.filters['strftime'] = custom_strftime
-    env.filters['strfdate'] = custom_strfdate
-    env.filters['strfdates'] = custom_strfdates
-    env.filters['weekday_abbrev'] = custom_weekday_abbrev
+    env = MyEnvironment(
+        **{"extensions": ["jinja2.ext.i18n", "jinja2.ext.debug"], **options}
+    )
+    env.globals.update(
+        {
+            "archive_mode": settings.ARCHIVE_MODE,
+            "browser_type": browser_type,
+            "css_scope": css_scope,
+            "active_theme": active_theme,
+            "get_language": get_language,
+            "get_messages": get_messages,
+            "json_script": json_script,
+            "hub_absolute": hub_absolute,
+            "hub_absolute_self": hub_absolute_self,
+            "num_of_unread_messages": num_of_unread_messages,
+            "num_of_pending_badges": num_of_pending_badges,
+            "static": static,
+            "url": url,
+            "show_vars": show_vars,
+            "unique_id": unique_id,
+            "translated_fields_for_field": translated_fields_for_field,
+            "field_translation_languages": field_translation_languages,
+            "now": timezone.now(),
+        }
+    )
+    env.filters["strftdelta"] = custom_timedelta
+    env.filters["strftdelta_short"] = custom_timedelta_short
+    env.filters["strftimehm"] = custom_strftimehm
+    env.filters["strftime"] = custom_strftime
+    env.filters["strfdate"] = custom_strfdate
+    env.filters["strfdates"] = custom_strfdates
+    env.filters["weekday_abbrev"] = custom_weekday_abbrev
     env.install_gettext_callables(gettext, ngettext, newstyle=True)
     return env
diff --git a/src/plainui/jinja2/plainui/assembly.html b/src/plainui/jinja2/plainui/assembly.html
index 8ed75d62dbb63f750eeee8a851b225b68cc0963e..eb8b9e080ba99e6e5f752eb97f37376afd74c9ee 100644
--- a/src/plainui/jinja2/plainui/assembly.html
+++ b/src/plainui/jinja2/plainui/assembly.html
@@ -41,6 +41,11 @@
                 <ul class="m-0 px-3 pb-0 pt-3 flex-grow-1 list-unstyled d-flex flex-row flex-wrap justify-content-start align-items-center">
                         {% for person in spokespeople %}
                             <li class="pe-3 mb-3">
+                                {% if not archive_mode %}
+                                    <a href="{{ url('plainui:user', user_slug=person.member.slug) }}" class="a a-bold">{{person.member.display_name}}</a>
+                                {% else %}
+                                    <label class="a a-bold">{{person.member.display_name}}</label>
+                                {% endif %}
                                 <a href="{{ url('plainui:user', user_slug=person.member.slug) }}" class="a a-bold">{{person.member.display_name}}</a>
                             </li>
                         {% endfor %}
diff --git a/src/plainui/jinja2/plainui/board.html b/src/plainui/jinja2/plainui/board.html
index 93fd8af7402a2a1ec8c7b68f32b72070d5f0d52b..22375c8433be501e0e5cb132773d4610f969a25f 100644
--- a/src/plainui/jinja2/plainui/board.html
+++ b/src/plainui/jinja2/plainui/board.html
@@ -5,6 +5,7 @@
 {% block content %}
     {{ titleMacro.title(_("Bulletin Board")) }}
 
+    {% if not archive_mode %}
     <div class="my-3">
         <ul class="mb-0 list-unstyled d-flex justify-content-center">
             {#<li class="col-12 mb-3">
@@ -81,5 +82,7 @@
     {%- endif %}
 
     <hr class="hub-spacer">
-
+    {% else %}{# archive mode #}
+      {{ _('archivemode_notavailable') }}
+    {% endif %}
 {% endblock %}
diff --git a/src/plainui/jinja2/plainui/components/function_btns.html b/src/plainui/jinja2/plainui/components/function_btns.html
index 3ca5005bc44bae9e7660cec743529a473ffcb2fa..80252b22e106e055ada341ae606a7b6ad11be83e 100644
--- a/src/plainui/jinja2/plainui/components/function_btns.html
+++ b/src/plainui/jinja2/plainui/components/function_btns.html
@@ -6,6 +6,7 @@
 #}
 
 {% macro fav(fav_id, fav_type, fav_is, color="transparent", next=request.get_full_path() ) -%}
+    {% if not archive_mode %}
     <form action="{{ url('plainui:modify_favorites') }}" class="d-inline-block" method="POST">
         {{ csrf_input }}
         <input type="hidden" name="next" value="{{ next ~ '#fav_' ~ fav_id}}">
@@ -30,9 +31,11 @@
             </button>
         {% endif -%}
     </form>
+    {% endif %}
 {%- endmacro %}
 
 {% macro schedule(sch_id, sch_is, color="transparent", next=request.get_full_path() ) -%}
+    {% if not archive_mode %}
     <form action="{{ url('plainui:modify_personal_calendar') }}" class="d-inline-block" method="POST">
         {{ csrf_input }}
         <input type="hidden" name="next" value="{{ next ~ '#sch_' ~ sch_id }}">
@@ -54,6 +57,7 @@
             </button>
         {% endif -%}
     </form>
+    {% endif %}
 {%- endmacro %}
 
 {% macro share(share_url, title=_("share this "), color="transparent" ) -%}
@@ -75,18 +79,22 @@
 {%- endmacro %}
 #
 {% macro report(report_url, kind="url", next=request.get_full_path() , title=_("report this url"), color="transparent" ) -%}
+    {% if not archive_mode %}
     <a href="{{ url('plainui:report_content') ~ '?kind=' ~ kind ~ '&kind_data=' ~ report_url | urlencode ~ '&next=' ~ next }}" class="me-2 btn-icon-big btn btn-{{ color }}" title="{{ title }}">
         <svg width="1.25rem" height="1.25rem" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" style="enable-background:new 0 0 500 500" fill="currentColor">
             <style>.st0{display:none}.st1{display:inline}.st2{fill:none;stroke:#000;stroke-width:18;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10}</style>
             <g><path d="M248.37 310.4c-9.86 0-16.38 1.9-19.56 5.7-3.19 3.8-4.78 10.01-4.78 18.63 0 8.83 1.59 15.1 4.78 18.79 3.18 3.7 9.7 5.54 19.56 5.54 9.85 0 16.32-1.85 19.4-5.54 3.08-3.7 4.62-9.96 4.62-18.79 0-8.62-1.53-14.83-4.62-18.63-3.08-3.8-9.55-5.7-19.4-5.7zM224.96 160.55c-1.23 1.34-1.85 2.93-1.85 4.78l10.78 126.29h29.57l10.47-126.29c0-1.85-.56-3.44-1.69-4.78-1.13-1.33-3.74-2-7.85-2h-32.03c-3.71 0-6.17.67-7.4 2z"/><path d="M448.65 237.65 262.3 51.3a16.992 16.992 0 0 0-24.04 0L51.9 237.65c-6.64 6.64-6.64 17.4 0 24.04l186.35 186.35a16.992 16.992 0 0 0 24.04 0l186.35-186.35c6.65-6.64 6.65-17.4.01-24.04zM250.27 411.98 87.96 249.67 250.28 87.36l162.31 162.31-162.32 162.31z"/></g>
         </svg>
     </a>
+    {% endif %}
 {%- endmacro %}
 
 {% macro edit(edit, title=_("edit this"), color="transparent" ) -%}
+    {% if not archive_mode %}
     <a href="{{ edit }}" class="me-2 btn-icon-big btn btn-{{ color }}" title="{{ title }}">
         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
           <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
         </svg>
     </a>
+    {% endif %}
 {%- endmacro %}
diff --git a/src/plainui/jinja2/plainui/components/header_buttons.html b/src/plainui/jinja2/plainui/components/header_buttons.html
index 285eeb4441bf88a370e20fe610aa33709a4f75e0..2257c9919ba6d0eff02cc3b2ec93ee0ee1037709 100644
--- a/src/plainui/jinja2/plainui/components/header_buttons.html
+++ b/src/plainui/jinja2/plainui/components/header_buttons.html
@@ -53,6 +53,7 @@
     </svg>
   </a>
   <ul class="dropdown-menu" aria-labelledby="lang-select">
+    {% if not archive_mode %}
     <form
         class="hub-header__additional-lang"
         method="POST"
@@ -75,6 +76,10 @@
             {{ _("en") }}
         </button></li>
     </form>
+    {% else %}
+      <li><a class="dropdown-item" href="{{ hub_absolute_self(request, lang='de') }}">{{ _("de") }}</a></li>
+      <li><a class="dropdown-item" href="{{ hub_absolute_self(request, lang='en') }}">{{ _("en") }}</a></li>
+    {% endif %}
   </ul>
 </div>
 {%- endmacro %}
@@ -139,7 +144,7 @@
 
 {% macro share(share_url, title=_("share this "), color="transparent" ) -%}
     <a
-        href="{{ share_url }}"
+        href="{{ hub_absolute_self(request, i18n=archive_mode) }}"
         class="btn-icon-big btn-{{ color }} nav-link"
         title="{{ title }}"
         target="_blank"
diff --git a/src/plainui/jinja2/plainui/dereferrer.html b/src/plainui/jinja2/plainui/dereferrer.html
index c62985d378efd9386b47937fafb6760e858c7717..d567e2c7b6381be4195333f91b1f1b84cdb18377 100644
--- a/src/plainui/jinja2/plainui/dereferrer.html
+++ b/src/plainui/jinja2/plainui/dereferrer.html
@@ -20,11 +20,11 @@
                 </a>
             </li>
             <li class="mx-2">
-                <a href='{{ url('plainui:dereferrer_approved', signed_payload=signed_url) }}' class="btn btn-primary " rel="external,noreferrer">
+                <a href='{{ url('plainui:dereferrer_approved', signed_payload=signed_url) if not archive_mode else plain_url }}' class="btn btn-primary " rel="external,noreferrer">
                     {{ _("Follow Link") }}
                 </a>
             </li>
-            {% if can_allow %}
+            {% if can_allow and not archive_mode %}
             <li class="col">
                 <a href="{{ url('plainui:dereferrer_save', signed_payload=signed_url) }}" class="btn btn-xl btn-block btn-primary external" rel="external,noreferrer">
                     {{ _("Follow & Allow '%(domain)s' permanently", domain=domain) }}
diff --git a/src/plainui/jinja2/plainui/login.html b/src/plainui/jinja2/plainui/login.html
index 37eb9cc9118b999b668c233ccd126ebd1c225e99..4a5f184c2506bb3a348c93232dfee83f318cba59 100644
--- a/src/plainui/jinja2/plainui/login.html
+++ b/src/plainui/jinja2/plainui/login.html
@@ -10,6 +10,7 @@
     </figure>
     <div class="mw-320 mx-auto">
         <h2>{{ _("Login") }}</h2>
+        {% if not archive_mode %}
         <form method="POST" id="login" class="hub-landing__form">
             <!--<h1 class="text-center bg-secondary p-2 text-dark h3 m-0">{{ _("login") }}</h1>-->
             <div class="">
@@ -31,7 +32,7 @@
                 <p class="mb-2 text-white">{{ _("login--cookieinfo") }}</p>
 
                 <hr class="hub-spacer">
-                <h3>{{ _("New here?") }}</h2>
+                <h3>{{ _("New here?") }}</h3>
                     <div class="d-grid">
                         <a href="{{ url('plainui:signup') }}" class="btn btn-primary" title="{{ _(" sign up (new
                             account)") }}">
@@ -40,6 +41,9 @@
                     </div>
             </div>
         </form>
+        {% else %}
+          <p class="mb-2 text-white">{{ _("archivemode_nologin") }}</p>
+        {% endif %}
     </div>
 
     <hr class="hub-spacer">
diff --git a/src/plainui/jinja2/plainui/room.html b/src/plainui/jinja2/plainui/room.html
index 7f65743cf6a47e40ab3a88cbde1febad5a3dc9fd..c97b384d6293e37eaa337e7d97a07edd785b886f 100644
--- a/src/plainui/jinja2/plainui/room.html
+++ b/src/plainui/jinja2/plainui/room.html
@@ -14,7 +14,8 @@
         <p>{{ _("Assembly") }}: <a href="{{ url('plainui:assembly', assembly_slug=room.assembly.slug) }}" class="a a-bold">{{room.assembly.name}}</a></p>
     {% endif %}
 
-    {% if voc_stream %}
+
+    {% if voc_stream and not archive_mode %}
         <div class="border m-0 p-0 bg-opaque">
             <h2 class="bg-secondary text-center text-dark m-0 px-3 py-1">{{ _("Currently Streaming") }}</h2>
             <div class="p-3">
diff --git a/src/plainui/jinja2/plainui/signup.html b/src/plainui/jinja2/plainui/signup.html
index 4d49361ac1413ec5510dc205a548d4a1492b5d53..f0bbb36cd17891f1d45294c0f6e6d806dedf3da6 100644
--- a/src/plainui/jinja2/plainui/signup.html
+++ b/src/plainui/jinja2/plainui/signup.html
@@ -11,6 +11,7 @@
 
     <p>{{ _("Registration Info Text") }}</p>
 
+    {% if not archive_mode %}
     <form
         method="POST"
         id="registration"
@@ -34,6 +35,9 @@
             </ul>
         </div>
     </form>
+    {% else %}
+      <p class="mb-2 text-white">{{ _("archivemode_nologin") }}</p>
+    {% endif %}
 
     <hr class="hub-spacer">
 </article>
diff --git a/src/plainui/jinja2/plainui/static_page.html b/src/plainui/jinja2/plainui/static_page.html
index 2d40b7e395f3d5a577fe11316d827f804a580936..7f5e66be2ae2a4147097abe4baf62892860eae61 100644
--- a/src/plainui/jinja2/plainui/static_page.html
+++ b/src/plainui/jinja2/plainui/static_page.html
@@ -23,6 +23,7 @@
     </div>
     {%- endif %}
     {{ titleMacro.title(title=page.title) }}
+    {% if not archive_mode %}
     <div class="row">
         <div class="col m-2 d-flex justify-content-start">
           <a href="{{ url('plainui:static_page_global_history') }}" class="btn btn-dark me-2">{{ _("Global History") }}</a>
@@ -38,6 +39,7 @@
             <a href="{{ url('plainui:static_page_history', page_slug=page_slug) }}" class="btn btn-dark">{{ _("History") }}</a>
         </div>
     </div>
+    {% endif %}
     <article class="pb-11">
         {{ markdownMacro.markdown(markdown=page_body | safe) }}
     </article>
diff --git a/src/plainui/locale/de/LC_MESSAGES/django.po b/src/plainui/locale/de/LC_MESSAGES/django.po
index 15851830692453d6ae6a9e5f503e2e7ef4c2e0bf..f1d0c7692b59261160a1e6063c93133d81f568b9 100644
--- a/src/plainui/locale/de/LC_MESSAGES/django.po
+++ b/src/plainui/locale/de/LC_MESSAGES/django.po
@@ -143,6 +143,9 @@ msgstr "Village Badges"
 msgid "No badges publicly available."
 msgstr "Es sind keine Badges öffentlich verfügbar. "
 
+msgid "archivemode_notavailable"
+msgstr "Diese Funktion steht nicht mehr zur Verfügung, da die Webseite archiviert wurde."
+
 msgid "Your Browser is broken. Get a better one here!"
 msgstr "Dein Browser ist defekt. Hier bekommst du einen Besseren!"
 
@@ -333,6 +336,9 @@ msgstr "Kapazität"
 msgid "Tags"
 msgstr "Tags"
 
+msgid "archivemode_loginlink_hint"
+msgstr "nicht verfügbar, Seite ist archiviert"
+
 msgid "no tags availaible"
 msgstr "Nicht getaggt."
 
@@ -438,6 +444,9 @@ msgstr "Neu hier?"
 msgid "sign up (new account)"
 msgstr "Account anlegen"
 
+msgid "archivemode_nologin"
+msgstr "Diese Webseite wurde archiviert, ein Login ist nicht mehr möglich."
+
 msgid "Profile"
 msgstr "Profil"
 
diff --git a/src/plainui/locale/en/LC_MESSAGES/django.po b/src/plainui/locale/en/LC_MESSAGES/django.po
index 5fcb31889a0dc5aa3e3455631e032e97dc46ee7c..b3007dca85de5af170ccefad3269a0f97ace0d1d 100644
--- a/src/plainui/locale/en/LC_MESSAGES/django.po
+++ b/src/plainui/locale/en/LC_MESSAGES/django.po
@@ -140,6 +140,9 @@ msgstr "village rooms"
 msgid "assembly badges"
 msgstr "village badges"
 
+msgid "archivemode_notavailable"
+msgstr "This function is not available any more as this website has been archived."
+
 msgid "No badges publicly available."
 msgstr ""
 
@@ -330,6 +333,9 @@ msgstr ""
 msgid "capacity"
 msgstr ""
 
+msgid "archivemode_loginlink_hint"
+msgstr "not available, page has been archived"
+
 msgid "Tags"
 msgstr ""
 
@@ -438,6 +444,9 @@ msgstr "New here?"
 msgid "sign up (new account)"
 msgstr "Sign up"
 
+msgid "archivemode_nologin"
+msgstr "This website has been archived, logging in is not possible any more."
+
 msgid "Profile"
 msgstr ""