diff --git a/src/backoffice/forms/assemblies.py b/src/backoffice/forms/assemblies.py
index 810ef2452b1d4e0cd64d81ad033f154ebdbb22e3..cb3d92f4d1f84daee102ded38c97a174cef9806a 100644
--- a/src/backoffice/forms/assemblies.py
+++ b/src/backoffice/forms/assemblies.py
@@ -1,8 +1,6 @@
 import logging
 
 from django import forms
-from django.core.exceptions import ValidationError
-from django.core.validators import validate_slug
 from django.utils.translation import gettext_lazy as _
 
 from core.base_forms import TranslatedFieldsForm
@@ -11,6 +9,7 @@ from core.models import (
     Assembly,
     AssemblyMember,
 )
+from core.utils import tags_from_string
 
 logger = logging.getLogger(__name__)
 
@@ -99,22 +98,7 @@ class AssemblyEditForm(TranslatedFieldsForm):
     tags = forms.CharField(required=False)
 
     def clean_tags(self):
-        # try to detect people delimiting tags with space instead of comma
-
-        tags = self.cleaned_data['tags']
-        # allow empty tags
-        if tags.strip() == '':
-            return ''
-
-        split_tags = tags.split(',')
-        if tags is not None and len(split_tags) == 1 and len(tags.split(' ')) > 2:
-            raise ValidationError(_('Assembly__tags__split_with_comma'))
-
-        for tag in split_tags:
-            stripped_tag = tag.strip()
-            validate_slug(stripped_tag)
-
-        return tags
+        return ','.join(tags_from_string(self.cleaned_data['tags']))
 
     def clean(self):
         # call original .clean() which e.g. removes 'slug' from cleaned_data if that isn't a slug
diff --git a/src/backoffice/forms/events.py b/src/backoffice/forms/events.py
index 91695593dbc0f6c14389ee2cf3e4d7fa8edfbb7d..6430fce6fe08df42235ba9375d2b34b0a77ee1a3 100644
--- a/src/backoffice/forms/events.py
+++ b/src/backoffice/forms/events.py
@@ -11,7 +11,7 @@ from core.models import (
     PlatformUser,
     Room,
 )
-from core.models.tags import clean_tags
+from core.utils import tags_from_string
 
 logger = logging.getLogger(__name__)
 
@@ -82,7 +82,7 @@ class EventForm(TranslatedFieldsForm):
             self.initial['tags_list'] = ', '.join(self.instance.sorted_tags)
 
     def clean_tags_list(self):
-        return clean_tags(self.cleaned_data['tags_list'])
+        return tags_from_string(self.cleaned_data['tags_list'])
 
     def clean(self):
         if self.cleaned_data.get('schedule_duration', None) is None:
diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po
index fb8bd7e1b18b1c56a1fd2675c2c3b81f086e725b..4a5930c3afce0405d0bbd352eb0550ceab916964 100644
--- a/src/backoffice/locale/de/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/de/LC_MESSAGES/django.po
@@ -23,9 +23,6 @@ msgstr "Bitte beachte den Hinweis und bestätige, dass du ihn gelesen und versta
 msgid "Assembly__slug__already_exists"
 msgstr "Dieser Kurzname wird bereits von einer anderen Assembly benutzt."
 
-msgid "Assembly__tags__split_with_comma"
-msgstr "Mehrere Tags bitte mit einem Komma trennen."
-
 msgid "username"
 msgstr "Benutzername"
 
@@ -434,6 +431,9 @@ msgstr "öffnet in neuem Fenster/Tab"
 msgid "Assembly__edit__children"
 msgstr "Zugeordnete Assemblies"
 
+msgid "Assembly__tags__split_with_comma"
+msgstr "Mehrere Tags bitte mit einem Komma trennen."
+
 msgid "Assembly__edit__submit_btn"
 msgstr "Speichern"
 
diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po
index a32d0eabdf1e2e8c505021a0d6e82c3313bd348a..75c2068e7f9a97865a8b1e550b11a2c42fff0cae 100644
--- a/src/backoffice/locale/en/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/en/LC_MESSAGES/django.po
@@ -23,9 +23,6 @@ msgstr "Please read the disclaimer and acknowledge that you have understood and
 msgid "Assembly__slug__already_exists"
 msgstr "This slug is already used by another assembly."
 
-msgid "Assembly__tags__split_with_comma"
-msgstr "Split multiple tags by comma."
-
 msgid "username"
 msgstr ""
 
@@ -434,6 +431,9 @@ msgstr "opens in a new tab or window"
 msgid "Assembly__edit__children"
 msgstr "Grouped Assemblies"
 
+msgid "Assembly__tags__split_with_comma"
+msgstr "Split multiple tags by comma."
+
 msgid "Assembly__edit__submit_btn"
 msgstr "Save"
 
diff --git a/src/backoffice/views/assemblies/assemblies.py b/src/backoffice/views/assemblies/assemblies.py
index 9269bf662e1a45dca3c32dd2a67fd14c6172e21e..daf9e1124ca5dbb707c087bb9e9fdac2dce190b6 100644
--- a/src/backoffice/views/assemblies/assemblies.py
+++ b/src/backoffice/views/assemblies/assemblies.py
@@ -255,7 +255,7 @@ class AssemblyUpdateView(AssemblyMixin, UpdateView):
         # update tags: go through supplied list of tags
         given_tags = form.cleaned_data.get('tags').split(',')
         for raw_tag in given_tags:
-            tag = raw_tag.strip()  # type: str
+            tag = raw_tag.strip().lower()  # type: str
             if len(tag) == 0:
                 # skip empty ones
                 continue
diff --git a/src/core/forms/projects.py b/src/core/forms/projects.py
index 7f07da402553d2645445acd4d68e1a1413613934..264a69b4f2e125fe2a30a6366ceac2a946d51e74 100644
--- a/src/core/forms/projects.py
+++ b/src/core/forms/projects.py
@@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _
 
 from core.base_forms import TranslatedFieldsForm
 from core.models import Assembly, PlatformUser, Project
-from core.models.tags import clean_tags
+from core.utils import tags_from_string
 
 
 class ProjectForm(TranslatedFieldsForm):
@@ -66,7 +66,7 @@ class ProjectForm(TranslatedFieldsForm):
         return self.cleaned_data['name']
 
     def clean_tags_list(self):
-        return clean_tags(self.cleaned_data['tags_list'])
+        return tags_from_string(self.cleaned_data['tags_list'])
 
     def clean(self) -> dict[str, Any]:
         if not self.create:
diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po
index 4649f5476a62c3e009274e7d2cc6ca218af080c5..fc92843e22a39728c411c3cd6f234bfe1a12016b 100644
--- a/src/core/locale/de/LC_MESSAGES/django.po
+++ b/src/core/locale/de/LC_MESSAGES/django.po
@@ -1969,9 +1969,6 @@ msgstr "erklärender Begleittext des Tags"
 msgid "ConferenceTag__description"
 msgstr "Beschreibung"
 
-msgid "Tags__split_with_comma"
-msgstr "Tags müssen mit Komma getrennt werden."
-
 msgid "ConferenceMemberTicket__token_wrong_conference"
 msgstr "Das Ticket ist nicht für diese Konferenz."
 
@@ -2469,6 +2466,9 @@ msgstr "Aktiviere dein %(safe_site_name)s Konto"
 msgid "Conference-day"
 msgstr "Tag"
 
+msgid "Tags__split_with_comma"
+msgstr "Tags müssen mit Komma getrennt werden."
+
 #, python-format
 msgid "Validation__error_file_size_MB_exceeded %(max_size)d"
 msgstr "Die Datei darf nicht größer als %(max_size)dMb sein"
diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po
index 794b6c1f7994a644428520491930c3322459fa71..dfb63b7f47bd35718e8786de74692374e07c9742 100644
--- a/src/core/locale/en/LC_MESSAGES/django.po
+++ b/src/core/locale/en/LC_MESSAGES/django.po
@@ -1967,9 +1967,6 @@ msgstr "additional explanation of this tag"
 msgid "ConferenceTag__description"
 msgstr "description"
 
-msgid "Tags__split_with_comma"
-msgstr "tags must be split with comma"
-
 msgid "ConferenceMemberTicket__token_wrong_conference"
 msgstr "This ticket is for another conference."
 
@@ -2451,6 +2448,9 @@ msgstr ""
 msgid "Conference-day"
 msgstr "day"
 
+msgid "Tags__split_with_comma"
+msgstr "tags must be split with comma"
+
 #, python-format
 msgid "Validation__error_file_size_MB_exceeded %(max_size)d"
 msgstr "You aren't allowed to upload a file larger than %(max_size)dMb"
diff --git a/src/core/models/tags.py b/src/core/models/tags.py
index 630e5512f6180a189962b010cd718fe48ee904f4..82dfa11cee0af553b8efe4d2bebb92d7f812bf68 100644
--- a/src/core/models/tags.py
+++ b/src/core/models/tags.py
@@ -3,7 +3,6 @@ import contextlib
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
-from django.core.validators import validate_slug
 from django.db import models
 from django.db.models import QuerySet
 from django.utils.functional import cached_property
@@ -193,20 +192,3 @@ class TaggedItemMixin:
         # TODO: check user's permission
         with contextlib.suppress(TagItem.DoesNotExist):
             self.tags.remove(self.tags.get(tag=tag))
-
-
-def clean_tags(tags: str):
-    # try to detect people delimiting tags with space instead of comma
-
-    # allow empty tags
-    if tags.strip() == '':
-        return ''
-
-    split_tags = tags.split(',')
-    if tags is not None and len(split_tags) == 1 and len(tags.split(' ')) > 2:
-        raise ValidationError(_('Tags__split_with_comma'))
-    stripped_tags = [tag.strip() for tag in split_tags]
-    for tag in stripped_tags:
-        validate_slug(tag)
-
-    return stripped_tags
diff --git a/src/core/utils.py b/src/core/utils.py
index c084a7b45eec011da92cf4228d05c4c86e7714fe..3ec4c560b60f602ff686d5680be68db9a6368dcd 100644
--- a/src/core/utils.py
+++ b/src/core/utils.py
@@ -12,10 +12,13 @@ from urllib.parse import parse_qs, urlparse, urlunparse
 
 import requests
 
+from django.core.exceptions import ValidationError
 from django.core.files.base import ContentFile
+from django.core.validators import validate_slug
 from django.urls import NoReverseMatch
 from django.utils.functional import cached_property
 from django.utils.html import strip_tags
+from django.utils.translation import gettext_lazy as _
 
 logger = logging.getLogger(__name__)
 
@@ -228,6 +231,22 @@ def mail2uuid(mail: str, prefix: str = 'acct:', suffix: str = '') -> uuid.UUID:
     return uuid.uuid5(uuid.NAMESPACE_URL, uri)
 
 
+def tags_from_string(value: str) -> list[str]:
+    # allow empty tags
+    if value.strip() == '':
+        return []
+
+    split_tags = value.split(',')
+    if value is not None and len(split_tags) == 1 and len(value.split(' ')) > 2:
+        raise ValidationError(_('Tags__split_with_comma'))
+
+    tags = [tag.strip().lower() for tag in split_tags]
+    for tag in tags:
+        validate_slug(tag)
+
+    return tags
+
+
 class GitCloneError(Exception):
     pass
 
diff --git a/src/plainui/views/general.py b/src/plainui/views/general.py
index bc3f1074e19ccc541411782fffbddfc26074fe79..bbe2e23d79419c7a46e565cbb1c539850390447d 100644
--- a/src/plainui/views/general.py
+++ b/src/plainui/views/general.py
@@ -11,7 +11,7 @@ __all__ = (
 from datetime import timedelta
 
 from django.contrib.contenttypes.models import ContentType
-from django.shortcuts import get_object_or_404, redirect
+from django.shortcuts import get_list_or_404, redirect
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.timezone import now
@@ -122,24 +122,25 @@ class TagView(ConferenceRequiredMixin, TemplateView):
         context = super().get_context_data(tag_slug=tag_slug, **kwargs)
         context['conf'] = self.conf
 
-        tag = get_object_or_404(ConferenceTag, slug=tag_slug)
-        context['tag'] = tag
+        # workaround: there can be multiple tags with different casing
+        tags = get_list_or_404(ConferenceTag, slug__iexact=tag_slug)
+        context['tag'] = tags[0]
 
         # TODO other types. What should we link here?
         # TODO: consider using views.utils.event_filter() here
         context['events'] = (
             Event.objects.conference_accessible(self.conf)
-            .filter(id__in=TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(Event)).values_list('target_id'))
+            .filter(id__in=TagItem.objects.filter(tag__in=tags, target_type=ContentType.objects.get_for_model(Event)).values_list('target_id'))
             .filter(schedule_start__isnull=False, schedule_end__isnull=False)
             .order_by('schedule_start', 'schedule_end')
         )
         context['my_favorite_events'] = session_get_favorite_events(self.request.session, self.request.user)
 
         context['assemblies'] = Assembly.objects.conference_accessible(self.conf).filter(
-            id__in=TagItem.objects.filter(tag=tag, target_type=ContentType.objects.get_for_model(Assembly)).values_list('target_id')
+            id__in=TagItem.objects.filter(tag__in=tags, target_type=ContentType.objects.get_for_model(Assembly)).values_list('target_id')
         )
         context['my_favorite_assemblies'] = session_get_favorite_assemblies(self.request.session, self.request.user)
-        context['projects'] = Project.objects.conference_accessible(self.conf).filter(tags__tag=tag)
+        context['projects'] = Project.objects.conference_accessible(self.conf).filter(tags__tag__in=tags)
         context['my_favorite_projects'] = session_get_favorite_projects(self.request.session, self.request.user)
 
         return context