diff --git a/app.vue b/app.vue
index c6b132ea..f378be87 100644
--- a/app.vue
+++ b/app.vue
@@ -1,21 +1,8 @@
 <script setup>
-import { APP_NAME } from './constants'
-
-const isDev = process.dev
-const isPreview = window.location.hostname.includes('deploy-preview')
-
-useHead({
-  titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${isDev ? ' (dev)' : isPreview ? ' (preview)' : ''}`,
-  link: [
-    { rel: 'icon', type: 'image/svg+png', href: isDev || isPreview ? '/favicon-dev.png' : '/favicon.png' },
-  ],
-})
+usePageHeader()
 
 // We want to trigger rerendering the page when account changes
 const key = computed(() => useMasto().instances.config.url || 'default')
-
-// eslint-disable-next-line no-unused-expressions
-isDark.value
 </script>
 
 <template>
@@ -27,19 +14,3 @@ isDark.value
     id="teleport-end"
   />
 </template>
-
-<style>
-html, body , #__nuxt{
-  height: 100vh;
-  margin: 0;
-  padding: 0;
-}
-
-html.dark {
-  color-scheme: dark;
-}
-
-html {
-  --at-apply: bg-base text-base;
-}
-</style>
diff --git a/composables/page-header.ts b/composables/page-header.ts
new file mode 100644
index 00000000..dce199d7
--- /dev/null
+++ b/composables/page-header.ts
@@ -0,0 +1,16 @@
+import { APP_NAME } from '~/constants'
+
+const isDev = process.dev
+const isPreview = window.location.hostname.includes('deploy-preview')
+
+export function usePageHeader() {
+  useHead({
+    titleTemplate: title => `${title ? `${title} | ` : ''}${APP_NAME}${isDev ? ' (dev)' : isPreview ? ' (preview)' : ''}`,
+    link: [
+      { rel: 'icon', type: 'image/svg+png', href: isDev || isPreview ? '/favicon-dev.png' : '/favicon.png' },
+    ],
+  })
+
+  // eslint-disable-next-line no-unused-expressions
+  isDark.value
+}
diff --git a/error.vue b/error.vue
new file mode 100644
index 00000000..bc5bd593
--- /dev/null
+++ b/error.vue
@@ -0,0 +1,57 @@
+<script setup lang="ts">
+import type { NuxtError } from '#app'
+
+// prevent reactive update when clearing error
+const { error } = defineProps<{
+  error: Partial<NuxtError>
+}>()
+
+usePageHeader()
+
+// add more custom status codes messages here
+const errorCodes: Record<number, string> = {
+  404: 'Page not found',
+}
+
+const defaultMessage = 'Something went wrong'
+
+const message = error.message ?? errorCodes[error.statusCode!] ?? defaultMessage
+
+const state = ref<'error' | 'reloading'>('error')
+const reload = async () => {
+  state.value = 'reloading'
+  try {
+    if (!useMasto())
+      await loginTo(currentUser.value)
+    clearError({ redirect: currentUser.value ? '/home' : '/public' })
+  }
+  catch {
+    state.value = 'error'
+  }
+}
+</script>
+
+<template>
+  <NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
+  <NuxtLayout>
+    <MainContent>
+      <template #title>
+        <span text-lg font-bold>Error</span>
+      </template>
+      <slot>
+        <form p5 grid gap-y-4 @submit="reload">
+          <div text-lg>
+            Something went wrong
+          </div>
+          <div text-secondary>
+            {{ message }}
+          </div>
+          <button flex items-center gap-2 justify-center btn-solid text-center :disabled="state === 'reloading'">
+            <span v-if="state === 'reloading'" i-ri:loader-2-fill animate-spin inline-block />
+            {{ state === 'reloading' ? 'Reloading' : 'Reload' }}
+          </button>
+        </form>
+      </slot>
+    </MainContent>
+  </NuxtLayout>
+</template>
diff --git a/nuxt.config.ts b/nuxt.config.ts
index af7efcf4..070c162a 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -55,6 +55,12 @@ export default defineNuxtConfig({
       translateApi: '',
     },
   },
+  nitro: {
+    prerender: {
+      crawlLinks: false,
+      routes: ['/200.html'],
+    },
+  },
   app: {
     keepalive: true,
   },
diff --git a/plugins/masto.ts b/plugins/masto.ts
index 875a2f3b..35dabe40 100644
--- a/plugins/masto.ts
+++ b/plugins/masto.ts
@@ -2,6 +2,7 @@ import type { MastoClient } from 'masto'
 import { currentUser } from '../composables/users'
 
 export default defineNuxtPlugin(async () => {
+  let masto!: MastoClient
   try {
     const { query } = useRoute()
     const user = typeof query.server === 'string' && typeof query.token === 'string'
@@ -9,23 +10,22 @@ export default defineNuxtPlugin(async () => {
       : currentUser.value
 
     // TODO: improve upstream to make this synchronous (delayed auth)
-    const masto = await loginTo(user) as MastoClient
-
-    return {
-      provide: {
-        masto: shallowReactive({
-          replace(api: MastoClient) { this.api = api },
-          api: masto,
-        }),
-      },
-    }
+    masto = await loginTo(user)
   }
   catch {
-    // TODO: handle error
     // Show error page when Mastodon server is down
-    throw createError({
+    showError({
       fatal: true,
       statusMessage: 'Could not log into account.',
     })
   }
+
+  return {
+    provide: {
+      masto: shallowReactive({
+        replace(api: MastoClient) { this.api = api },
+        api: masto,
+      }),
+    },
+  }
 })
diff --git a/styles/global.css b/styles/global.css
index 81590eb9..ef78c278 100644
--- a/styles/global.css
+++ b/styles/global.css
@@ -103,3 +103,17 @@ html {
     background-position: 0 50%
   }
 }
+
+html, body , #__nuxt{
+  height: 100vh;
+  margin: 0;
+  padding: 0;
+}
+
+html.dark {
+  color-scheme: dark;
+}
+
+html {
+  --at-apply: bg-base text-base;
+}