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>