diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 30bbda0a7ffe2413c48af6494aaba91a0975d981..3cc6a013323e220ff05e15c48aa4eac6885014a2 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,6 +14,8 @@ Added
 
 * GraphQL schema for Rooms
 * [Dev] UpdateIndicator Vue Component to display the status of interactive pages
+* [Dev] DeleteDialog Vue Component to unify item deletion in the new frontend
+
 
 Changed
 ~~~~~~~
@@ -31,6 +33,8 @@ Fixed
 * The `Stop Impersonation` button is not shown due to an oversee when changing the type of the whoAmI query to an object of UserType
 * Offline fallback page for legacy pages was misleading sometimes.
 * Route changes in the Legacy-Component iframe didn't trigger a scroll to the top
+* Query strings did not get passed when navigating legacy pages inside of the SPA.
+* Retry button on error 500 page did not trigger a reload of the page.
 * When the Celery worker wasn't able to execute all tasks in time, notifications were sent multiple times.
 
 `3.0b3`_ - 2023-03-19
diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
index 84f7e2e12be16f868c731b06f11ad0e4d2557ef4..790fc10c592bac5acc1e3bbadabcae63e065804f 100644
--- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue
+++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue
@@ -59,13 +59,14 @@ export default {
       const location = this.$refs.contentIFrame.contentWindow.location;
       const url = new URL(location);
       const path = url.pathname.replace(/^\/django/, "");
+      const pathWithQueryString = path + encodeURI(url.search);
       const routePath =
         path.charAt(path.length - 1) === "/" &&
         this.$route.path.charAt(path.length - 1) !== "/"
           ? this.$route.path + "/"
           : this.$route.path;
       if (path !== routePath) {
-        this.$router.push(path);
+        this.$router.push(pathWithQueryString);
       }
 
       // Show loader if iframe starts to change its content, even if the $route stays the same
diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a56e3960cd1ae29e60f966a44ae69adeb0332949
--- /dev/null
+++ b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue
@@ -0,0 +1,136 @@
+<template>
+  <ApolloMutation
+    v-if="dialogOpen"
+    :mutation="gqlMutation"
+    :variables="{ id: item.id }"
+    :update="update"
+    @done="close(true)"
+  >
+    <template #default="{ mutate, loading, error }">
+      <v-dialog v-model="dialogOpen" max-width="500px">
+        <v-card>
+          <v-card-title class="text-h5">
+            <slot name="title">
+              {{ $t("actions.confirm_deletion") }}
+            </slot>
+          </v-card-title>
+          <v-card-text>
+            <slot name="body">
+              <p class="text-body-1">{{ nameOfObject }}</p>
+            </slot>
+          </v-card-text>
+          <v-card-actions>
+            <v-spacer></v-spacer>
+            <v-btn text @click="close(false)" :disabled="loading">
+              <slot name="cancelContent">
+                {{ $t("actions.cancel") }}
+              </slot>
+            </v-btn>
+            <v-btn
+              color="error"
+              text
+              @click="mutate"
+              :loading="loading"
+              :disabled="loading"
+            >
+              <slot name="deleteContent">
+                {{ $t("actions.delete") }}
+              </slot>
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-dialog>
+      <v-snackbar :value="error !== null">
+        {{ error }}
+
+        <template #action="{ attrs }">
+          <v-btn color="primary" text v-bind="attrs" @click="error = null" icon>
+            <v-icon>$close</v-icon>
+          </v-btn>
+        </template>
+      </v-snackbar>
+    </template>
+  </ApolloMutation>
+</template>
+
+<script>
+export default {
+  name: "DeleteDialog",
+  computed: {
+    nameOfObject() {
+      return this.itemAttribute in this.item || {}
+        ? this.item[this.itemAttribute]
+        : this.item.toString();
+    },
+    dialogOpen: {
+      get() {
+        return this.value;
+      },
+
+      set(val) {
+        this.$emit("input", val);
+      },
+    },
+  },
+  methods: {
+    update(store) {
+      if (!this.gqlQuery) {
+        // There is no GraphQL query to update
+        return;
+      }
+
+      // Read the data from cache for query
+      const storedData = store.readQuery({ query: this.gqlQuery });
+
+      if (!storedData) {
+        // There are no data in the cache yet
+        return;
+      }
+
+      const storedDataKey = Object.keys(storedData)[0];
+
+      // Remove item from stored data
+      const index = storedData[storedDataKey].findIndex(
+        (m) => m.id === this.item.id
+      );
+      storedData[storedDataKey].splice(index, 1);
+
+      // Write data back to the cache
+      store.writeQuery({ query: this.gqlQuery, data: storedData });
+    },
+    close(success) {
+      this.$emit("input", false);
+      if (success) {
+        this.$emit("success");
+      } else {
+        this.$emit("cancel");
+      }
+    },
+  },
+  props: {
+    value: {
+      type: Boolean,
+      required: true,
+    },
+    item: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+    itemAttribute: {
+      type: String,
+      required: false,
+      default: "name",
+    },
+    gqlMutation: {
+      type: Object,
+      required: true,
+    },
+    gqlQuery: {
+      type: Object,
+      required: false,
+      default: null,
+    },
+  },
+};
+</script>
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index ff9693d369fea8f9ce44e07d5cc1dd4b89aae1aa..5235e23265346a5842cccf793e67987459a5b40f 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -75,7 +75,12 @@
     "back": "Back",
     "search": "Search",
     "edit": "Edit",
-    "close": "Close"
+    "close": "Close",
+    "cancel": "Cancel",
+    "confirm_deletion": "Are you sure you want to delete this item?",
+    "delete": "Delete",
+    "stop_editing": "Stop editing",
+    "save": "Save"
   },
   "administration": {
     "backend_admin": {
diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py
index e927dadccaaab7b2bf39c2110286970222f70f5b..e3478a34ff62be4859d8d9e88a8d73322796ad82 100644
--- a/aleksis/core/schema/base.py
+++ b/aleksis/core/schema/base.py
@@ -1,3 +1,6 @@
+from django.db.models import Model
+from django.core.exceptions import PermissionDenied
+
 import graphene
 from graphene_django import DjangoObjectType
 
@@ -24,3 +27,23 @@ class FieldFileType(graphene.ObjectType):
 
     def resolve_absolute_url(root, info, **kwargs):
         return info.context.build_absolute_uri(root.url) if root else ""
+
+
+class DeleteMutation(graphene.Mutation):
+    """Mutation to delete an object."""
+
+    klass: Model = None
+    permission_required: str = ""
+    ok = graphene.Boolean()
+
+    class Arguments:
+        id = graphene.ID()  # noqa
+
+    @classmethod
+    def mutate(cls, root, info, **kwargs):
+        obj = cls.klass.objects.get(pk=kwargs["id"])
+        if info.context.user.has_perm(cls.permission_required, obj):
+            obj.delete()
+            return cls(ok=True)
+        else:
+            raise PermissionDenied()
diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html
index d008cd5405f4e5eee73ddf175cab87057e563785..a2ea5c902dba89f34241403e85d0e8af8d8f9a5d 100644
--- a/aleksis/core/templates/500.html
+++ b/aleksis/core/templates/500.html
@@ -16,7 +16,7 @@
           {% endblocktrans %}
         </p>
         {% include "core/partials/admins_list.html" %}
-        <a href="javascript:window.location.reload()" class="btn green waves-effect waves-light">
+        <a onClick="window.location.reload();" class="btn secondary waves-effect waves-light">
           <i class="material-icons left">refresh</i>
           {% trans "Retry" %}
         </a>
diff --git a/pyproject.toml b/pyproject.toml
index 9a3318eaef990a7445fd14bcbabad0885b1ac6b2..e4de1fd18c27222094738b4ff926335dc9f9a3c0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -100,7 +100,7 @@ django-favicon-plus-reloaded = "^1.1.5"
 django-health-check = "^3.12.1"
 psutil = "^5.7.0"
 celery-progress = "^0.1.0"
-django-cachalot = "^2.3.2"
+django-cachalot = "^2.5.3"
 django-prometheus = "^2.1.0"
 django-model-utils = "^4.0.0"
 bs4 = "^0.0.1"