diff --git a/aleksis/core/assets/components/generic/DetailView.vue b/aleksis/core/assets/components/generic/DetailView.vue
new file mode 100644
index 0000000000000000000000000000000000000000..843dacefab4c49f2c2f827c42b4b56f7aa49b9b5
--- /dev/null
+++ b/aleksis/core/assets/components/generic/DetailView.vue
@@ -0,0 +1,55 @@
+<template>
+  <div>
+    <v-row class="align-center">
+      <v-col
+        v-if="!noAvatar"
+        cols="5"
+        sm="4"
+        md="3"
+        lg="2"
+        xl="1"
+        order="first"
+        max-width="220px"
+      >
+        <slot name="avatarContent" />
+      </v-col>
+
+      <v-col order="last" order-sm="1" cols="12" sm="">
+        <h1>
+          <slot name="title" />
+        </h1>
+
+        <div class="text-h5 grey--text text--darken-2">
+          <slot name="subtitle" />
+        </div>
+      </v-col>
+
+      <v-col order="1" order-sm="last" class="ms-5">
+        <div
+          class="d-flex gap justify-md-end flex-column-reverse flex-md-row align-end align-md-center"
+        >
+          <slot name="actions" />
+        </div>
+      </v-col>
+    </v-row>
+    <slot />
+  </div>
+</template>
+
+<script>
+export default {
+  name: "DetailView",
+  props: {
+    noAvatar: {
+      type: Boolean,
+      required: false,
+    },
+  },
+};
+</script>
+
+<style scoped>
+.gap {
+  gap: 0.5rem;
+}
+</style>
diff --git a/aleksis/core/assets/components/generic/ListView.vue b/aleksis/core/assets/components/generic/ListView.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7a78a7643b2f08846f341677eb82074eda31013e
--- /dev/null
+++ b/aleksis/core/assets/components/generic/ListView.vue
@@ -0,0 +1,28 @@
+<template>
+  <detail-view no-avatar>
+    <template #title>
+      <slot name="title" />
+    </template>
+
+    <template #actions>
+      <slot name="actions" />
+    </template>
+
+    <slot name="filter" />
+    <slot />
+  </detail-view>
+</template>
+
+<script>
+import DetailView from "./DetailView.vue";
+export default {
+  name: "ListView",
+  components: {
+    DetailView,
+  },
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/aleksis/core/assets/components/group/GroupList.vue b/aleksis/core/assets/components/group/GroupCollection.vue
similarity index 100%
rename from aleksis/core/assets/components/group/GroupList.vue
rename to aleksis/core/assets/components/group/GroupCollection.vue
diff --git a/aleksis/core/assets/components/person/PersonActions.vue b/aleksis/core/assets/components/person/PersonActions.vue
index d042cf7cd08d80e7f7c7510227583c1316f8a622..2b0bc7bc6e55d985b6c66c3d7b9dbc92b13344b4 100644
--- a/aleksis/core/assets/components/person/PersonActions.vue
+++ b/aleksis/core/assets/components/person/PersonActions.vue
@@ -1,20 +1,17 @@
 <template>
   <ApolloQuery :query="require('./personActions.graphql')" :variables="{ id }">
     <template #default="{ result: { error, data, loading } }">
-      <div
-        class="d-flex gap justify-md-end flex-column-reverse flex-md-row align-end align-md-center"
-      >
-        <v-skeleton-loader v-if="loading" type="actions" />
-        <template v-else-if="data && data.person && data.person.id">
-          <v-btn
+      <v-skeleton-loader v-if="loading" type="actions"/>
+      <template v-else-if="data && data.person && data.person.id">
+        <v-btn
             v-if="data.person.canEditPerson"
             color="primary"
             :to="{ name: 'core.editPerson', params: { id: data.person.id } }"
-          >
-            <v-icon left>$edit</v-icon>
-            {{ $t("actions.edit") }}
-          </v-btn>
-          <v-btn
+        >
+          <v-icon left>$edit</v-icon>
+          {{ $t("actions.edit") }}
+        </v-btn>
+        <v-btn
             v-if="data.person.canChangePersonPreferences"
             color="secondary"
             outlined
@@ -23,74 +20,82 @@
               name: 'core.preferencesPersonByPk',
               params: { pk: data.person.id },
             }"
-          >
-            <v-icon left>$preferences</v-icon>
-            {{ $t("preferences.person.change_preferences") }}
-          </v-btn>
+        >
+          <v-icon left>$preferences</v-icon>
+          {{ $t("preferences.person.change_preferences") }}
+        </v-btn>
 
-          <v-menu>
-            <template #activator="{ on, attrs }">
-              <v-btn outlined text v-bind="attrs" v-on="on">
-                <v-icon center>mdi-dots-horizontal</v-icon>
-              </v-btn>
-            </template>
-            <v-list>
-              <v-list-item
+        <v-menu
+            v-if="
+              data.person.canImpersonatePerson ||
+              data.person.canInvitePerson ||
+              data.person.canDeletePerson
+            "
+        >
+          <template #activator="{ on, attrs }">
+            <v-btn outlined text v-bind="attrs" v-on="on">
+              <v-icon center>mdi-dots-horizontal</v-icon>
+            </v-btn>
+          </template>
+          <v-list>
+            <v-list-item
                 v-if="data.person.canImpersonatePerson"
                 :to="{
                   name: 'impersonate.impersonateByUserPk',
                   params: { uid: data.person.userid },
                   query: { next: $route.path },
                 }"
-              >
-                <v-list-item-icon>
-                  <v-icon>mdi-account-box-outline</v-icon>
-                </v-list-item-icon>
-                <v-list-item-content>
-                  <v-list-item-title>{{
+            >
+              <v-list-item-icon>
+                <v-icon>mdi-account-box-outline</v-icon>
+              </v-list-item-icon>
+              <v-list-item-content>
+                <v-list-item-title>{{
                     $t("person.impersonation.impersonate")
-                  }}</v-list-item-title>
-                </v-list-item-content>
-              </v-list-item>
+                  }}
+                </v-list-item-title>
+              </v-list-item-content>
+            </v-list-item>
 
-              <v-list-item
+            <v-list-item
                 v-if="data.person.canInvitePerson"
                 :to="{
                   name: 'core.invitePerson',
                   params: { id: data.person.id },
                 }"
-              >
-                <v-list-item-icon>
-                  <v-icon>mdi-account-plus-outline</v-icon>
-                </v-list-item-icon>
-                <v-list-item-content>
-                  <v-list-item-title>{{
+            >
+              <v-list-item-icon>
+                <v-icon>mdi-account-plus-outline</v-icon>
+              </v-list-item-icon>
+              <v-list-item-content>
+                <v-list-item-title>{{
                     $t("person.invite")
-                  }}</v-list-item-title>
-                </v-list-item-content>
-              </v-list-item>
+                  }}
+                </v-list-item-title>
+              </v-list-item-content>
+            </v-list-item>
 
-              <v-list-item
+            <v-list-item
                 v-if="data.person.canDeletePerson"
                 :to="{
                   name: 'core.deletePerson',
                   params: { id: data.person.id },
                 }"
                 class="error--text"
-              >
-                <v-list-item-icon>
-                  <v-icon color="error">mdi-delete</v-icon>
-                </v-list-item-icon>
-                <v-list-item-content>
-                  <v-list-item-title>{{
+            >
+              <v-list-item-icon>
+                <v-icon color="error">mdi-delete</v-icon>
+              </v-list-item-icon>
+              <v-list-item-content>
+                <v-list-item-title>{{
                     $t("person.delete")
-                  }}</v-list-item-title>
-                </v-list-item-content>
-              </v-list-item>
-            </v-list>
-          </v-menu>
-        </template>
-      </div>
+                  }}
+                </v-list-item-title>
+              </v-list-item-content>
+            </v-list-item>
+          </v-list>
+        </v-menu>
+      </template>
     </template>
   </ApolloQuery>
 </template>
@@ -108,7 +113,5 @@ export default {
 </script>
 
 <style scoped>
-.gap {
-  gap: 0.5rem;
-}
+
 </style>
diff --git a/aleksis/core/assets/components/person/PersonList.vue b/aleksis/core/assets/components/person/PersonCollection.vue
similarity index 100%
rename from aleksis/core/assets/components/person/PersonList.vue
rename to aleksis/core/assets/components/person/PersonCollection.vue
diff --git a/aleksis/core/assets/components/person/PersonOverview.vue b/aleksis/core/assets/components/person/PersonOverview.vue
index dd55ed7181d72c10a117dc2c96619852f8325efc..d7990c3ff6f17fb7df7e3f60cb30f0f24c5a4451 100644
--- a/aleksis/core/assets/components/person/PersonOverview.vue
+++ b/aleksis/core/assets/components/person/PersonOverview.vue
@@ -11,183 +11,176 @@
         </v-row>
       </template>
       <template v-else-if="data && data.person">
-        <v-row class="align-center">
-          <v-col
-            cols="5"
-            sm="4"
-            md="3"
-            lg="2"
-            xl="1"
-            order="first"
-            max-width="220px"
-          >
+        <detail-view>
+          <template #avatarContent>
             <avatar-click-box :id="id" />
-          </v-col>
-          <v-col order="last" order-sm="1" cols="12" sm="">
-            <h1>{{ data.person.firstName }} {{ data.person.lastName }}</h1>
-            <div
-              v-if="data.person.username"
-              class="text-h5 grey--text text--darken-2"
-            >
-              {{ data.person.username }}
-            </div>
-          </v-col>
-
-          <v-col order="1" order-sm="last" class="ms-5">
-            <person-actions :id="data.person.id" />
-          </v-col>
-        </v-row>
-
-        <div class="text-center my-5" v-text="data.person.description"></div>
+          </template>
 
-        <v-row>
-          <v-col cols="12" lg="4">
-            <v-card class="mb-6">
-              <v-card-title>{{ $t("person.details") }}</v-card-title>
-
-              <v-list two-line>
-                <v-list-item>
-                  <v-list-item-icon>
-                    <v-icon> mdi-account-outline </v-icon>
-                  </v-list-item-icon>
-
-                  <v-list-item-content>
-                    <v-list-item-title>
-                      {{ data.person.firstName }}
-                      {{ data.person.additionalName }}
-                      {{ data.person.lastName }}
-                    </v-list-item-title>
-                  </v-list-item-content>
-                </v-list-item>
-                <v-divider inset />
-
-                <v-list-item>
-                  <v-list-item-icon>
-                    <v-icon> mdi-human-non-binary </v-icon>
-                  </v-list-item-icon>
-
-                  <v-list-item-content>
-                    <v-list-item-title>
-                      {{ data.person.sex || "–" }}
-                    </v-list-item-title>
-                  </v-list-item-content>
-                </v-list-item>
-                <v-divider inset />
-
-                <v-list-item>
-                  <v-list-item-icon>
-                    <v-icon> mdi-map-marker-outline </v-icon>
-                  </v-list-item-icon>
-
-                  <v-list-item-content>
-                    <v-list-item-title
-                      >{{ data.person.street || "–" }}
-                      {{ data.person.housenumber }}</v-list-item-title
-                    >
-                    <v-list-item-subtitle
-                      >{{ data.person.postalCode }}
-                      {{ data.person.place }}</v-list-item-subtitle
-                    >
-                  </v-list-item-content>
-                </v-list-item>
-                <v-divider inset />
-
-                <v-list-item :href="'tel:' + data.person.phoneNumber">
-                  <v-list-item-icon>
-                    <v-icon> mdi-phone-outline </v-icon>
-                  </v-list-item-icon>
-
-                  <v-list-item-content>
-                    <v-list-item-title>{{
-                      data.person.phoneNumber || "–"
-                    }}</v-list-item-title>
-                    <v-list-item-subtitle>{{
-                      $t("person.home")
-                    }}</v-list-item-subtitle>
-                  </v-list-item-content>
-                </v-list-item>
-
-                <v-list-item :href="'tel:' + data.person.mobileNumber">
-                  <v-list-item-action></v-list-item-action>
-
-                  <v-list-item-content>
-                    <v-list-item-title>{{
-                      data.person.mobileNumber || "–"
-                    }}</v-list-item-title>
-                    <v-list-item-subtitle>{{
-                      $t("person.mobile")
-                    }}</v-list-item-subtitle>
-                  </v-list-item-content>
-                </v-list-item>
-                <v-divider inset />
-
-                <v-list-item :href="'mailto:' + data.person.email">
-                  <v-list-item-icon>
-                    <v-icon> mdi-email-outline </v-icon>
-                  </v-list-item-icon>
-
-                  <v-list-item-content>
-                    <v-list-item-title>
-                      {{ data.person.email || "–" }}
-                    </v-list-item-title>
-                  </v-list-item-content>
-                </v-list-item>
-                <v-divider inset />
-
-                <v-list-item>
-                  <v-list-item-icon>
-                    <v-icon> mdi-cake-variant-outline </v-icon>
-                  </v-list-item-icon>
-
-                  <v-list-item-content>
-                    <v-list-item-title>{{
-                      !!data.person.dateOfBirth
-                        ? $d(new Date(data.person.dateOfBirth), "short")
-                        : "–"
-                    }}</v-list-item-title>
-                    <v-list-item-subtitle>{{
-                      data.person.placeOfBirth
-                    }}</v-list-item-subtitle>
-                  </v-list-item-content>
-                </v-list-item>
-              </v-list>
-            </v-card>
-
-            <additional-image :src="data.person.secondaryImageUrl" />
-          </v-col>
+          <template #title>
+            {{ data.person.firstName }} {{ data.person.lastName }}
+          </template>
 
-          <v-col
-            cols="12"
-            md="6"
-            lg="4"
-            v-if="data.person.children.length || data.person.guardians.length"
-          >
-            <v-card v-if="data.person.children.length" class="mb-6">
-              <v-card-title>{{ $t("person.children") }}</v-card-title>
-              <person-list :persons="data.person.children" />
-            </v-card>
-            <v-card v-if="data.person.guardians.length">
-              <v-card-title>{{ $t("person.guardians") }}</v-card-title>
-              <person-list :persons="data.person.guardians" />
-            </v-card>
-          </v-col>
+          <template #subtitle>
+            {{ data.person.username }}
+          </template>
 
-          <v-col
-            cols="12"
-            md="6"
-            lg="4"
-            v-if="data.person.memberOf.length || data.person.ownerOf.length"
-          >
-            <v-card v-if="data.person.memberOf.length" class="mb-6">
-              <v-card-title>{{ $t("group.title_plural") }}</v-card-title>
-              <group-list :groups="data.person.memberOf" />
-            </v-card>
-            <v-card v-if="data.person.ownerOf.length">
-              <v-card-title>{{ $t("group.ownership") }}</v-card-title>
-              <group-list :groups="data.person.ownerOf" />
-            </v-card>
-          </v-col>
-        </v-row>
+          <template #actions>
+            <person-actions :id="data.person.id" />
+          </template>
+
+          <div class="text-center my-5" v-text="data.person.description"></div>
+
+          <v-row>
+            <v-col cols="12" lg="4">
+              <v-card class="mb-6">
+                <v-card-title>{{ $t("person.details") }}</v-card-title>
+
+                <v-list two-line>
+                  <v-list-item>
+                    <v-list-item-icon>
+                      <v-icon> mdi-account-outline</v-icon>
+                    </v-list-item-icon>
+
+                    <v-list-item-content>
+                      <v-list-item-title>
+                        {{ data.person.firstName }}
+                        {{ data.person.additionalName }}
+                        {{ data.person.lastName }}
+                      </v-list-item-title>
+                    </v-list-item-content>
+                  </v-list-item>
+                  <v-divider inset />
+
+                  <v-list-item>
+                    <v-list-item-icon>
+                      <v-icon> mdi-human-non-binary</v-icon>
+                    </v-list-item-icon>
+
+                    <v-list-item-content>
+                      <v-list-item-title>
+                        {{ data.person.sex || "–" }}
+                      </v-list-item-title>
+                    </v-list-item-content>
+                  </v-list-item>
+                  <v-divider inset />
+
+                  <v-list-item>
+                    <v-list-item-icon>
+                      <v-icon> mdi-map-marker-outline</v-icon>
+                    </v-list-item-icon>
+
+                    <v-list-item-content>
+                      <v-list-item-title
+                        >{{ data.person.street || "–" }}
+                        {{ data.person.housenumber }}
+                      </v-list-item-title>
+                      <v-list-item-subtitle
+                        >{{ data.person.postalCode }}
+                        {{ data.person.place }}
+                      </v-list-item-subtitle>
+                    </v-list-item-content>
+                  </v-list-item>
+                  <v-divider inset />
+
+                  <v-list-item :href="'tel:' + data.person.phoneNumber">
+                    <v-list-item-icon>
+                      <v-icon> mdi-phone-outline</v-icon>
+                    </v-list-item-icon>
+
+                    <v-list-item-content>
+                      <v-list-item-title
+                        >{{ data.person.phoneNumber || "–" }}
+                      </v-list-item-title>
+                      <v-list-item-subtitle
+                        >{{ $t("person.home") }}
+                      </v-list-item-subtitle>
+                    </v-list-item-content>
+                  </v-list-item>
+
+                  <v-list-item :href="'tel:' + data.person.mobileNumber">
+                    <v-list-item-action></v-list-item-action>
+
+                    <v-list-item-content>
+                      <v-list-item-title
+                        >{{ data.person.mobileNumber || "–" }}
+                      </v-list-item-title>
+                      <v-list-item-subtitle
+                        >{{ $t("person.mobile") }}
+                      </v-list-item-subtitle>
+                    </v-list-item-content>
+                  </v-list-item>
+                  <v-divider inset />
+
+                  <v-list-item :href="'mailto:' + data.person.email">
+                    <v-list-item-icon>
+                      <v-icon> mdi-email-outline</v-icon>
+                    </v-list-item-icon>
+
+                    <v-list-item-content>
+                      <v-list-item-title>
+                        {{ data.person.email || "–" }}
+                      </v-list-item-title>
+                    </v-list-item-content>
+                  </v-list-item>
+                  <v-divider inset />
+
+                  <v-list-item>
+                    <v-list-item-icon>
+                      <v-icon> mdi-cake-variant-outline</v-icon>
+                    </v-list-item-icon>
+
+                    <v-list-item-content>
+                      <v-list-item-title
+                        >{{
+                          !!data.person.dateOfBirth
+                            ? $d(new Date(data.person.dateOfBirth), "short")
+                            : "–"
+                        }}
+                      </v-list-item-title>
+                      <v-list-item-subtitle
+                        >{{ data.person.placeOfBirth }}
+                      </v-list-item-subtitle>
+                    </v-list-item-content>
+                  </v-list-item>
+                </v-list>
+              </v-card>
+
+              <additional-image :src="data.person.secondaryImageUrl" />
+            </v-col>
+
+            <v-col
+              cols="12"
+              md="6"
+              lg="4"
+              v-if="data.person.children.length || data.person.guardians.length"
+            >
+              <v-card v-if="data.person.children.length" class="mb-6">
+                <v-card-title>{{ $t("person.children") }}</v-card-title>
+                <person-collection :persons="data.person.children" />
+              </v-card>
+              <v-card v-if="data.person.guardians.length">
+                <v-card-title>{{ $t("person.guardians") }}</v-card-title>
+                <person-collection :persons="data.person.guardians" />
+              </v-card>
+            </v-col>
+
+            <v-col
+              cols="12"
+              md="6"
+              lg="4"
+              v-if="data.person.memberOf.length || data.person.ownerOf.length"
+            >
+              <v-card v-if="data.person.memberOf.length" class="mb-6">
+                <v-card-title>{{ $t("group.title_plural") }}</v-card-title>
+                <group-collection :groups="data.person.memberOf" />
+              </v-card>
+              <v-card v-if="data.person.ownerOf.length">
+                <v-card-title>{{ $t("group.ownership") }}</v-card-title>
+                <group-collection :groups="data.person.ownerOf" />
+              </v-card>
+            </v-col>
+          </v-row>
+        </detail-view>
       </template>
     </template>
   </ApolloQuery>
@@ -196,18 +189,20 @@
 <script>
 import AdditionalImage from "./AdditionalImage.vue";
 import AvatarClickBox from "./AvatarClickBox.vue";
-import GroupList from "../group/GroupList.vue";
+import DetailView from "../generic/DetailView.vue";
+import GroupCollection from "../group/GroupCollection.vue";
 import PersonActions from "./PersonActions.vue";
-import PersonList from "./PersonList.vue";
+import PersonCollection from "./PersonCollection.vue";
 
 export default {
   name: "PersonOverview",
   components: {
     AdditionalImage,
     AvatarClickBox,
-    GroupList,
+    DetailView,
+    GroupCollection,
     PersonActions,
-    PersonList,
+    PersonCollection,
   },
   props: {
     id: {
diff --git a/aleksis/core/management/commands/convert_urls_to_routes.py b/aleksis/core/management/commands/convert_urls_to_routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..71404415d6c536a8be1e59a80d0be977d3a4be70
--- /dev/null
+++ b/aleksis/core/management/commands/convert_urls_to_routes.py
@@ -0,0 +1,93 @@
+from re import sub
+
+from django.apps import apps
+from django.core.management.base import BaseCommand, CommandError
+
+from aleksis.core.util.core_helpers import get_app_module
+
+
+def camelcase(value: str) -> str:
+    """Convert a string to camelcase."""
+    titled = value.replace("_", " ").title().replace(" ", "")
+    return titled[0].lower() + titled[1:]
+
+
+class Command(BaseCommand):
+    help = "Convert Django URLs for an app into vue-router routes"  # noqa
+
+    def add_arguments(self, parser):
+        parser.add_argument("app", type=str)
+
+    def handle(self, *args, **options):
+        app = options["app"]
+        app_camel_case = camelcase(app)
+
+        app_config = apps.get_app_config(app)
+        app_config_name = f"{app_config.__module__}.{app_config.__class__.__name__}"
+
+        # Import urls from app
+        urls = get_app_module(app_config_name, "urls")
+        if not urls:
+            raise CommandError(f"No url patterns found in app {app}")
+        urlpatterns = urls.urlpatterns
+
+        # Import menu from app and structure as dict by url name
+        menus = get_app_module(app_config_name, "menus")
+        menu_by_urls = {}
+        if "NAV_MENU_CORE" in menus.MENUS:
+            menu = menus.MENUS["NAV_MENU_CORE"]
+            menu_by_urls = {m["url"]: m for m in menu}
+
+            for menu_item in menu:
+                if "submenu" in menu_item:
+                    for submenu_item in menu_item["submenu"]:
+                        menu_by_urls[submenu_item["url"]] = submenu_item
+
+        for url in urlpatterns:
+            # Convert route name and url pattern to vue-router format
+            menu = menu_by_urls[url.name] if url.name in menu_by_urls else None
+            route_name = f"{app_camel_case}.{camelcase(url.name)}"
+            url_pattern = url.pattern._route
+            url_pattern = sub(r"<(?P<val>\w+)>", r":\g<val>", url_pattern)
+
+            # Start building route
+            route = "{\n"
+            route += f'  path: "{url_pattern}",\n'
+            route += '  component: () => import("./components/LegacyBaseTemplate.vue"),\n'
+            route += f'  name: "{route_name}",\n'
+
+            if menu:
+                # Convert icon to Vuetify format
+                icon = None
+                if menu.get("vuetify_icon"):
+                    icon = menu["vuetify_icon"]
+                elif menu.get("svg_icon"):
+                    icon = menu["svg_icon"].replace(":", "-")
+                elif menu.get("icon"):
+                    icon = "mdi-" + menu["icon"]
+
+                if icon:
+                    icon = icon.replace("_", "-")
+
+                # Get permission for menu item
+                permission = None
+                if menu.get("validators"):
+                    possible_validators = [
+                        v
+                        for v in menu["validators"]
+                        if v[0] == "aleksis.core.util.predicates.permission_validator"
+                    ]
+                    if possible_validators:
+                        permission = possible_validators[0][1]
+
+                route += "  meta: {{\n"
+                route += "    inMenu: true,\n"
+                route += f'    titleKey: "{menu["name"]}", // Needs manual work\n'
+                if icon:
+                    route += f'    icon: "{icon}",\n'
+                if permission:
+                    route += f'    permission: "{permission}",\n'
+                route += "  }},\n"
+            route += "},"
+
+            print(route)
diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py
index b51fa08c2da1032210d5f641a8f309190b1b49ff..821cade46c4ca76539e284bd1311a6992869e782 100644
--- a/aleksis/core/schema/person.py
+++ b/aleksis/core/schema/person.py
@@ -47,6 +47,71 @@ class PersonType(DjangoObjectType):
     can_impersonate_person = graphene.Boolean()
     can_invite_person = graphene.Boolean()
 
+    def resolve_street(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_address_rule", root):
+            return root.street
+        return None
+
+    def resolve_housenumber(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_address_rule", root):
+            return root.housenumber
+        return None
+
+    def resolve_postal_code(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_address_rule", root):
+            return root.postal_code
+        return None
+
+    def resolve_place(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_address_rule", root):
+            return root.place
+        return None
+
+    def resolve_phone_number(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_contact_details_rule", root):
+            return root.phone_number
+        return None
+
+    def resolve_mobile_number(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_contact_details_rule", root):
+            return root.mobile_number
+        return None
+
+    def resolve_email(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_contact_details_rule", root):
+            return root.email
+        return None
+
+    def resolve_date_of_birth(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_personal_details_rule", root):
+            return root.date_of_birth
+        return None
+
+    def resolve_place_of_birth(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_personal_details_rule", root):
+            return root.place_of_birth
+        return None
+
+    def resolve_children(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_personal_details_rule", root):
+            return root.children.all()
+        return []
+
+    def resolve_guardians(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_personal_details_rule", root):
+            return root.guardians.all()
+        return []
+
+    def resolve_member_of(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_person_groups_rule", root):
+            return root.member_of.all()
+        return []
+
+    def resolve_owner_of(root, info, **kwargs):  # noqa
+        if info.context.user.has_perm("core.view_person_groups_rule", root):
+            return root.owner_of.all()
+        return []
+
     def resolve_username(root, info, **kwargs):  # noqa
         return root.user.username if root.user else None
 
diff --git a/aleksis/core/templates/core/partials/pure_css_loader.html b/aleksis/core/templates/core/partials/pure_css_loader.html
deleted file mode 100644
index 581752415a29448fdff998861530ac51a6877622..0000000000000000000000000000000000000000
--- a/aleksis/core/templates/core/partials/pure_css_loader.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{# Loader by https://loading.io/css/ under CC0 licence #}
-
-<style>
-  .wrapper {
-    width: 100vw;
-    height: 100vh;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  .lds-ring {
-    display: inline-block;
-    position: relative;
-    width: 80px;
-    height: 80px;
-  }
-
-  .lds-ring div {
-    box-sizing: border-box;
-    display: block;
-    position: absolute;
-    width: 64px;
-    height: 64px;
-    margin: 8px;
-    border: 4px solid{{ request.site.preferences.theme__primary }};
-    border-radius: 50%;
-    animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
-    border-color: {{ request.site.preferences.theme__primary }} transparent transparent transparent;
-  }
-
-  .lds-ring div:nth-child(1) {
-    animation-delay: -0.45s;
-  }
-
-  .lds-ring div:nth-child(2) {
-    animation-delay: -0.3s;
-  }
-
-  .lds-ring div:nth-child(3) {
-    animation-delay: -0.15s;
-  }
-
-  @keyframes lds-ring {
-    0% {
-      transform: rotate(0deg);
-    }
-    100% {
-      transform: rotate(360deg);
-    }
-  }
-</style>
-<div class="wrapper">
-  <div class="lds-ring">
-    <div></div>
-    <div></div>
-    <div></div>
-    <div></div>
-  </div>
-</div>
diff --git a/aleksis/core/templates/core/partials/splash_screen.html b/aleksis/core/templates/core/partials/splash_screen.html
new file mode 100644
index 0000000000000000000000000000000000000000..00e01649894a2a83c7d0129580034350fed77c3a
--- /dev/null
+++ b/aleksis/core/templates/core/partials/splash_screen.html
@@ -0,0 +1,115 @@
+{% load static any_js i18n %}
+{% include_css "Roboto300" %}
+{% static "img/aleksis-banner.svg" as aleksis_banner %}
+<div id="logo-container">
+  <img
+    src="{% firstof request.site.preferences.theme__logo.url aleksis_banner %}"
+    alt="{{ request.site.preferences.general__title }} – Logo"
+    id="logo"
+    width="600"
+  >
+  <div id="text">
+    <h1>{{ request.site.preferences.general__title }}</h1>
+    <div class="lds-ellipsis">
+      <div></div>
+      <div></div>
+      <div></div>
+      <div></div>
+    </div>
+  </div>
+  <noscript>
+    {% blocktrans %}
+      This webbrowser doesn't support JavaScript, or it's execution is blocked. Please use another browser to continue.
+    {% endblocktrans %}
+  </noscript>
+</div>
+
+<style>
+  #logo {
+    width: 100%;
+  }
+
+  #logo-container {
+    width: min(80vw, 600px);
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+  }
+
+  #text {
+    display: flex;
+    justify-content: space-around;
+    flex-wrap: wrap;
+  }
+
+  h1 {
+    font-family: Roboto, sans-serif;
+    font-weight: 300;
+  }
+
+  .lds-ellipsis {
+    display: inline-block;
+    position: relative;
+    width: 80px;
+    height: 80px;
+  }
+
+  .lds-ellipsis div {
+    position: absolute;
+    top: 33px;
+    width: 13px;
+    height: 13px;
+    border-radius: 50%;
+    background: {{ request.site.preferences.theme__primary }};
+    animation-timing-function: cubic-bezier(0, 1, 1, 0);
+  }
+
+  .lds-ellipsis div:nth-child(1) {
+    left: 8px;
+    animation: lds-ellipsis1 0.6s infinite;
+  }
+
+  .lds-ellipsis div:nth-child(2) {
+    left: 8px;
+    animation: lds-ellipsis2 0.6s infinite;
+  }
+
+  .lds-ellipsis div:nth-child(3) {
+    left: 32px;
+    animation: lds-ellipsis2 0.6s infinite;
+  }
+
+  .lds-ellipsis div:nth-child(4) {
+    left: 56px;
+    animation: lds-ellipsis3 0.6s infinite;
+  }
+
+  @keyframes lds-ellipsis1 {
+    0% {
+      transform: scale(0);
+    }
+    100% {
+      transform: scale(1);
+    }
+  }
+
+  @keyframes lds-ellipsis3 {
+    0% {
+      transform: scale(1);
+    }
+    100% {
+      transform: scale(0);
+    }
+  }
+
+  @keyframes lds-ellipsis2 {
+    0% {
+      transform: translate(0, 0);
+    }
+    100% {
+      transform: translate(24px, 0);
+    }
+  }
+
+</style>
diff --git a/aleksis/core/templates/core/vue_index.html b/aleksis/core/templates/core/vue_index.html
index ea4f99b042be25cfd93ee463f5278da80b4042bd..f362963d9d137f3ab3f4886116f0e693018e843f 100644
--- a/aleksis/core/templates/core/vue_index.html
+++ b/aleksis/core/templates/core/vue_index.html
@@ -22,7 +22,7 @@
   <body>
     <main id="app">
       <!-- HTML and CSS in #app will be replaced by vue -->
-      {% include "core/partials/pure_css_loader.html" %}
+      {% include "core/partials/splash_screen.html" %}
 
       <app ref="aleksisApp"></app>
     </main>