diff --git a/aleksis/core/frontend/app/apollo.js b/aleksis/core/frontend/app/apollo.js
index c1cf08a8b3510f19dae8dc1e7bc8b49f72fd2fb1..10f63e2d5e48e0383af2d07fdd4f33259ee5a119 100644
--- a/aleksis/core/frontend/app/apollo.js
+++ b/aleksis/core/frontend/app/apollo.js
@@ -63,7 +63,7 @@ const apolloOpts = {
       error: ({ graphQLErrors, networkError }, vm) => {
         if (graphQLErrors) {
           for (let err of graphQLErrors) {
-            console.error("GraphQL query error:", err.message);
+            console.error("GraphQL query error in query", err.path.join(".") , ":", err.message);
           }
           // Add a snackbar on all errors returned by the GraphQL endpoint
           //  If App is offline, don't add snackbar since only the ping query is active
diff --git a/aleksis/core/frontend/components/generic/ObjectOverview.vue b/aleksis/core/frontend/components/generic/ObjectOverview.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7f7498de7e575ebab0ca7fe71efbc2883cb7702c
--- /dev/null
+++ b/aleksis/core/frontend/components/generic/ObjectOverview.vue
@@ -0,0 +1,69 @@
+<template>
+  <div>
+    <slot name="loading" v-if="$apollo.queries.object.loading"></slot>
+    <slot v-else-if="object" v-bind="object"></slot>
+    <error-page
+        v-else
+        :shortErrorMessageKey="shortErrorMessageKey"
+        :longErrorMessageKey="longErrorMessageKey"
+    />
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: "ObjectOverview",
+  props: {
+    titleAttr: {
+      type: String,
+      required: true,
+    },
+    query: {
+      type: Object,
+      required: true,
+    },
+    shortErrorMessageKey: {
+      type: String,
+      required: false,
+      default: "network_errors.error_404",
+    },
+    longErrorMessageKey: {
+      type: String,
+      required: false,
+      default: "network_errors.page_not_found",
+    },
+  },
+  methods: {
+    getTitleAttr(obj) {
+      let tmpObj = obj;
+      this.titleAttr.split(".").forEach((attr) => {tmpObj = tmpObj[attr]})
+      return tmpObj;
+    }
+  },
+  apollo: {
+    object() {
+      return {
+        query: this.query,
+        variables() {
+          if (this.$route.params.id) {
+            return {
+              id: this.$route.params.id,
+            };
+          }
+          return {};
+        },
+        result({data}) {
+          if (data && data.object) {
+            this.$root.$setPageTitle(this.getTitleAttr(data.object));
+          }
+        },
+      };
+    },
+  },
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/aleksis/core/frontend/components/person/PersonOverview.vue b/aleksis/core/frontend/components/person/PersonOverview.vue
index 978b6689a2e6b777e1e05fbb1859b8651eb3782e..5171a22f0ed594bb849d82d8bab671db7e1207ec 100644
--- a/aleksis/core/frontend/components/person/PersonOverview.vue
+++ b/aleksis/core/frontend/components/person/PersonOverview.vue
@@ -1,6 +1,9 @@
 <template>
-  <div>
-    <template v-if="$apollo.queries.person.loading">
+  <object-overview
+      :query="query"
+      title-attr="fullName"
+  >
+    <template #loading>
       <v-skeleton-loader type="article" />
 
       <v-row>
@@ -9,7 +12,7 @@
         </v-col>
       </v-row>
     </template>
-    <template v-else-if="person">
+    <template v-slot="person">
       <detail-view>
         <template #avatarContent>
           <person-avatar-clickbox :id="id" />
@@ -189,12 +192,13 @@
         </v-row>
       </detail-view>
     </template>
-  </div>
+  </object-overview>
 </template>
 
 <script>
 import AdditionalImage from "./AdditionalImage.vue";
 import GroupCollection from "../group/GroupCollection.vue";
+import ObjectOverview from "../generic/ObjectOverview.vue";
 import PersonActions from "./PersonActions.vue";
 import PersonAvatarClickbox from "./PersonAvatarClickbox.vue";
 import PersonCollection from "./PersonCollection.vue";
@@ -206,27 +210,15 @@ export default {
   components: {
     AdditionalImage,
     GroupCollection,
+    ObjectOverview,
     PersonActions,
     PersonAvatarClickbox,
     PersonCollection,
   },
-  apollo: {
-    person: {
+  data() {
+    return {
       query: gqlPersonOverview,
-      variables() {
-        if (this.$route.params.id) {
-          return {
-            id: this.$route.params.id,
-          };
-        }
-        return {};
-      },
-      result({ data }) {
-        if (data && data.person) {
-          this.$root.$setPageTitle(data.person.fullName);
-        }
-      },
-    },
+    }
   },
   props: {
     id: {
diff --git a/aleksis/core/frontend/components/person/personOverview.graphql b/aleksis/core/frontend/components/person/personOverview.graphql
index cbf77ef19e48346007d7ebab43f6325a6ded3d3f..8173f6a7d7d74842a267c4d5c0a47d7d3ed97634 100644
--- a/aleksis/core/frontend/components/person/personOverview.graphql
+++ b/aleksis/core/frontend/components/person/personOverview.graphql
@@ -1,5 +1,5 @@
 query person($id: ID) {
-  person: personByIdOrMe(id: $id) {
+  object: personByIdOrMe(id: $id) {
     id
     username
     firstName
diff --git a/aleksis/core/vite.config.js b/aleksis/core/vite.config.js
index fcaf916e41decd4053c9805b8e310b9baa1a2ad8..b459ae1f3b761875a59de70dfdf39cb9a26e32ca 100644
--- a/aleksis/core/vite.config.js
+++ b/aleksis/core/vite.config.js
@@ -97,6 +97,22 @@ function generateAppImporter(appDetails) {
   return code;
 }
 
+/**
+ * Generate a mapping of esbuild import aliases for apps.
+ *
+ * App code locations are discovered by the `aleksis-admin` vite wrapper and passed
+ * in the django_values hints.
+ */
+function generateAppAliases(appDetails) {
+  let aliases = { "aleksis.core": django_values.coreAssetDir };
+
+  for (const [appPackage, appMeta] of Object.entries(appDetails)) {
+    aliases[appPackage] = appMeta.assetDir;
+  }
+
+  return aliases;
+}
+
 export default defineConfig({
   // root must always be the base directory of the AlekSIS-Core source tree
   //  Changing this will mangle the manifest key of the entrypoint!
@@ -293,6 +309,8 @@ export default defineConfig({
     alias: {
       "@": path.resolve(django_values.node_modules),
       vue: path.resolve(django_values.node_modules + "/vue/dist/vue.esm.js"),
+      // Add aliases for every app using their package name
+      ...generateAppAliases(django_values.appDetails),
     },
   },
 });