diff --git a/app.vue b/app.vue
index e0ffddf9..4d44ed25 100644
--- a/app.vue
+++ b/app.vue
@@ -6,12 +6,15 @@ provideGlobalCommands()
 
 // We want to trigger rerendering the page when account changes
 const key = computed(() => `${currentUser.value?.server ?? currentServer.value}:${currentUser.value?.account.id || ''}`)
+
+const { params } = useRoute()
 </script>
 
 <template>
   <NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
   <NuxtLayout :key="key">
-    <NuxtPage v-if="isMastoInitialised" />
+    <!-- TODO: rework the /[account] routes to remove conditional loading -->
+    <NuxtPage v-if="(!params.account && $route.path !== '/signin/callback') || isMastoInitialised" />
   </NuxtLayout>
   <AriaAnnouncer />
 </template>
diff --git a/components/common/CommonRouteTabs.vue b/components/common/CommonRouteTabs.vue
index 65d8864b..d596b889 100644
--- a/components/common/CommonRouteTabs.vue
+++ b/components/common/CommonRouteTabs.vue
@@ -5,6 +5,7 @@ const { options, command, replace, preventScrollTop = false } = $defineProps<{
   options: {
     to: RouteLocationRaw
     display: string
+    disabled?: boolean
     name?: string
     icon?: string
   }[]
@@ -28,18 +29,25 @@ useCommands(() => command
 
 <template>
   <div flex w-full items-center lg:text-lg of-x-auto scrollbar-hide>
-    <NuxtLink
+    <template
       v-for="(option, index) in options"
       :key="option?.name || index"
-      :to="option.to"
-      :replace="replace"
-      relative flex flex-auto cursor-pointer sm:px6 px2 rounded transition-all
-      tabindex="1"
-      hover:bg-active transition-100
-      exact-active-class="children:(font-bold !border-primary !op100)"
-      @click="!preventScrollTop && $scrollToTop()"
     >
-      <span ws-nowrap mxa sm:px2 sm:py3 py2 text-center border-b-3 op50 hover:op70 border-transparent>{{ option.display }}</span>
-    </NuxtLink>
+      <NuxtLink
+        v-if="!option.disabled"
+        :to="option.to"
+        :replace="replace"
+        relative flex flex-auto cursor-pointer sm:px6 px2 rounded transition-all
+        tabindex="1"
+        hover:bg-active transition-100
+        exact-active-class="children:(text-secondary !border-primary !op100)"
+        @click="!preventScrollTop && $scrollToTop()"
+      >
+        <span ws-nowrap mxa sm:px2 sm:py3 py2 text-center border-b-3 text-secondary-light hover:text-secondary border-transparent>{{ option.display }}</span>
+      </NuxtLink>
+      <div v-else flex flex-auto sm:px6 px2>
+        <span ws-nowrap mxa sm:px2 sm:py3 py2 text-center text-secondary-light op50>{{ option.display }}</span>
+      </div>
+    </template>
   </div>
 </template>
diff --git a/components/timeline/TimelineBlocks.vue b/components/timeline/TimelineBlocks.vue
new file mode 100644
index 00000000..5b20b337
--- /dev/null
+++ b/components/timeline/TimelineBlocks.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const paginator = useMasto().blocks.iterate()
+</script>
+
+<template>
+  <AccountPaginator :paginator="paginator" />
+</template>
diff --git a/components/timeline/TimelineBookmarks.vue b/components/timeline/TimelineBookmarks.vue
new file mode 100644
index 00000000..f99ba34c
--- /dev/null
+++ b/components/timeline/TimelineBookmarks.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const paginator = useMasto().bookmarks.iterate()
+</script>
+
+<template>
+  <TimelinePaginator :paginator="paginator" />
+</template>
diff --git a/components/timeline/TimelineConversations.vue b/components/timeline/TimelineConversations.vue
new file mode 100644
index 00000000..a9af6ba3
--- /dev/null
+++ b/components/timeline/TimelineConversations.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const paginator = useMasto().conversations.iterate()
+</script>
+
+<template>
+  <ConversationPaginator :paginator="paginator" />
+</template>
diff --git a/components/timeline/TimelineDomainBlocks.vue b/components/timeline/TimelineDomainBlocks.vue
new file mode 100644
index 00000000..a0f564f8
--- /dev/null
+++ b/components/timeline/TimelineDomainBlocks.vue
@@ -0,0 +1,21 @@
+<script setup lang="ts">
+const masto = useMasto()
+const paginator = masto.domainBlocks.iterate()
+
+const unblock = async (domain: string) => {
+  await masto.domainBlocks.unblock(domain)
+}
+</script>
+
+<template>
+  <CommonPaginator :paginator="paginator">
+    <template #default="{ item }">
+      <CommonDropdownItem class="!cursor-auto">
+        {{ item }}
+        <template #actions>
+          <div i-ri:lock-unlock-line text-primary cursor-pointer @click="unblock(item)" />
+        </template>
+      </CommonDropdownItem>
+    </template>
+  </CommonPaginator>
+</template>
diff --git a/components/timeline/TimelineFavourites.vue b/components/timeline/TimelineFavourites.vue
new file mode 100644
index 00000000..b5e286a5
--- /dev/null
+++ b/components/timeline/TimelineFavourites.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const paginator = useMasto().favourites.iterate()
+</script>
+
+<template>
+  <TimelinePaginator :paginator="paginator" />
+</template>
diff --git a/components/timeline/TimelineHome.vue b/components/timeline/TimelineHome.vue
new file mode 100644
index 00000000..c9792b9b
--- /dev/null
+++ b/components/timeline/TimelineHome.vue
@@ -0,0 +1,12 @@
+<script setup lang="ts">
+const paginator = useMasto().timelines.iterateHome()
+const stream = await useMasto().stream.streamUser()
+onBeforeUnmount(() => stream.disconnect())
+</script>
+
+<template>
+  <div>
+    <PublishWidget draft-key="home" border="b base" />
+    <TimelinePaginator v-bind="{ paginator, stream }" context="home" />
+  </div>
+</template>
diff --git a/components/timeline/TimelineMentions.vue b/components/timeline/TimelineMentions.vue
new file mode 100644
index 00000000..8e0a925b
--- /dev/null
+++ b/components/timeline/TimelineMentions.vue
@@ -0,0 +1,13 @@
+<script setup lang="ts">
+// Default limit is 20 notifications, and servers are normally caped to 30
+const paginator = useMasto().notifications.iterate({ limit: 30, types: ['mention'] })
+
+const { clearNotifications } = useNotifications()
+onActivated(clearNotifications)
+
+const stream = await useMasto().stream.streamUser()
+</script>
+
+<template>
+  <NotificationPaginator v-bind="{ paginator, stream }" />
+</template>
diff --git a/components/timeline/TimelineMutes.vue b/components/timeline/TimelineMutes.vue
new file mode 100644
index 00000000..303c10ff
--- /dev/null
+++ b/components/timeline/TimelineMutes.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const paginator = useMasto().mutes.iterate()
+</script>
+
+<template>
+  <AccountPaginator :paginator="paginator" />
+</template>
diff --git a/components/timeline/TimelineNotifications.vue b/components/timeline/TimelineNotifications.vue
new file mode 100644
index 00000000..53c067ec
--- /dev/null
+++ b/components/timeline/TimelineNotifications.vue
@@ -0,0 +1,13 @@
+<script setup lang="ts">
+// Default limit is 20 notifications, and servers are normally caped to 30
+const paginator = useMasto().notifications.iterate({ limit: 30 })
+
+const { clearNotifications } = useNotifications()
+onActivated(clearNotifications)
+
+const stream = await useMasto().stream.streamUser()
+</script>
+
+<template>
+  <NotificationPaginator v-bind="{ paginator, stream }" />
+</template>
diff --git a/components/timeline/TimelinePinned.vue b/components/timeline/TimelinePinned.vue
new file mode 100644
index 00000000..e0862f01
--- /dev/null
+++ b/components/timeline/TimelinePinned.vue
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const paginator = useMasto().accounts.iterateStatuses(currentUser.value!.account.id, { pinned: true })
+</script>
+
+<template>
+  <TimelinePaginator :paginator="paginator" />
+</template>
diff --git a/pages/[[server]]/explore.vue b/pages/[[server]]/explore.vue
index eed38e1b..ca3446a7 100644
--- a/pages/[[server]]/explore.vue
+++ b/pages/[[server]]/explore.vue
@@ -17,15 +17,11 @@ const tabs = $computed(() => [
     display: t('tab.news'),
   },
   // This section can only be accessed after logging in
-  ...invoke(() => currentUser.value
-    ? [
-        {
-          to: `/${currentServer.value}/explore/users`,
-          display: t('tab.for_you'),
-        },
-      ]
-    : [],
-  ),
+  {
+    to: `/${currentServer.value}/explore/users`,
+    display: t('tab.for_you'),
+    disabled: !isMastoInitialised.value || !currentUser.value,
+  },
 ] as const)
 </script>
 
@@ -41,6 +37,6 @@ const tabs = $computed(() => [
     <template #header>
       <CommonRouteTabs replace :options="tabs" />
     </template>
-    <NuxtPage />
+    <NuxtPage v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/blocks.vue b/pages/blocks.vue
index 010e2012..5e460657 100644
--- a/pages/blocks.vue
+++ b/pages/blocks.vue
@@ -3,8 +3,6 @@ definePageMeta({
   middleware: 'auth',
 })
 
-const paginator = useMasto().blocks.iterate()
-
 useHeadFixed({
   title: 'Blocked users',
 })
@@ -15,6 +13,7 @@ useHeadFixed({
     <template #title>
       <span text-lg font-bold>{{ $t('account.blocked_users') }}</span>
     </template>
-    <AccountPaginator :paginator="paginator" />
+
+    <TimelineBlocks v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/bookmarks.vue b/pages/bookmarks.vue
index fd578689..c1921a5c 100644
--- a/pages/bookmarks.vue
+++ b/pages/bookmarks.vue
@@ -3,8 +3,6 @@ definePageMeta({
   middleware: 'auth',
 })
 
-const paginator = useMasto().bookmarks.iterate()
-
 const { t } = useI18n()
 
 useHeadFixed({
@@ -21,8 +19,6 @@ useHeadFixed({
       </NuxtLink>
     </template>
 
-    <slot>
-      <TimelinePaginator :paginator="paginator" />
-    </slot>
+    <TimelineBookmarks v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/conversations.vue b/pages/conversations.vue
index 31d54ec3..13654581 100644
--- a/pages/conversations.vue
+++ b/pages/conversations.vue
@@ -3,8 +3,6 @@ definePageMeta({
   middleware: 'auth',
 })
 
-const paginator = useMasto().conversations.iterate()
-
 const { t } = useI18n()
 
 useHeadFixed({
@@ -21,8 +19,6 @@ useHeadFixed({
       </NuxtLink>
     </template>
 
-    <slot>
-      <ConversationPaginator :paginator="paginator" />
-    </slot>
+    <TimelineConversations v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/domain_blocks.vue b/pages/domain_blocks.vue
index ed25088f..e91b7e16 100644
--- a/pages/domain_blocks.vue
+++ b/pages/domain_blocks.vue
@@ -1,18 +1,13 @@
 <script setup lang="ts">
+import TimelineDomainBlocks from '~~/components/timeline/TimelineDomainBlocks.vue'
+
 definePageMeta({
   middleware: 'auth',
 })
 
-const masto = useMasto()
-const paginator = masto.domainBlocks.iterate()
-
 useHeadFixed({
   title: 'Blocked domains',
 })
-
-const unblock = async (domain: string) => {
-  await masto.domainBlocks.unblock(domain)
-}
 </script>
 
 <template>
@@ -21,15 +16,6 @@ const unblock = async (domain: string) => {
       <span text-lg font-bold>{{ $t('account.blocked_domains') }}</span>
     </template>
 
-    <CommonPaginator :paginator="paginator">
-      <template #default="{ item }">
-        <CommonDropdownItem class="!cursor-auto">
-          {{ item }}
-          <template #actions>
-            <div i-ri:lock-unlock-line text-primary cursor-pointer @click="unblock(item)" />
-          </template>
-        </CommonDropdownItem>
-      </template>
-    </CommonPaginator>
+    <TimelineDomainBlocks v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/favourites.vue b/pages/favourites.vue
index b0f97722..23685701 100644
--- a/pages/favourites.vue
+++ b/pages/favourites.vue
@@ -3,7 +3,6 @@ definePageMeta({
   middleware: 'auth',
 })
 
-const paginator = useMasto().favourites.iterate()
 const { t } = useI18n()
 
 useHeadFixed({
@@ -19,8 +18,7 @@ useHeadFixed({
         <span>{{ t('nav_side.favourites') }}</span>
       </NuxtLink>
     </template>
-    <slot>
-      <TimelinePaginator :paginator="paginator" />
-    </slot>
+
+    <TimelineFavourites v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/home.vue b/pages/home.vue
index f0249840..22c0fcd4 100644
--- a/pages/home.vue
+++ b/pages/home.vue
@@ -6,16 +6,6 @@ definePageMeta({
   alias: ['/signin/callback'],
 })
 
-if (useRoute().path === '/signin/callback') {
-  // This only cleans up the URL; page content should stay the same
-  useRouter().push('/home')
-}
-
-const masto = useMasto()
-const paginator = masto.timelines.iterateHome()
-const stream = await masto.stream.streamUser()
-onBeforeUnmount(() => stream.disconnect())
-
 const { t } = useI18n()
 useHeadFixed({
   title: () => t('nav_side.home'),
@@ -30,9 +20,7 @@ useHeadFixed({
         <span>{{ $t('nav_side.home') }}</span>
       </NuxtLink>
     </template>
-    <slot>
-      <PublishWidget draft-key="home" border="b base" />
-      <TimelinePaginator v-bind="{ paginator, stream }" context="home" />
-    </slot>
+
+    <TimelineHome v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/mutes.vue b/pages/mutes.vue
index c26a24a9..4e773bb7 100644
--- a/pages/mutes.vue
+++ b/pages/mutes.vue
@@ -2,9 +2,6 @@
 definePageMeta({
   middleware: 'auth',
 })
-
-const paginator = useMasto().mutes.iterate()
-
 useHeadFixed({
   title: 'Muted users',
 })
@@ -15,6 +12,7 @@ useHeadFixed({
     <template #title>
       <span text-lg font-bold>{{ $t('account.muted_users') }}</span>
     </template>
-    <AccountPaginator :paginator="paginator" />
+
+    <TimelineMutes v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/pages/notifications.vue b/pages/notifications.vue
index 4e6e63ea..795b4577 100644
--- a/pages/notifications.vue
+++ b/pages/notifications.vue
@@ -53,6 +53,7 @@ onActivated(() => {
       <template v-if="pwaEnabled">
         <NotificationPreferences :show="showSettings" />
       </template>
+
       <NuxtPage />
     </slot>
   </MainContent>
diff --git a/pages/notifications/index.vue b/pages/notifications/index.vue
index 08bf4aab..ce586d62 100644
--- a/pages/notifications/index.vue
+++ b/pages/notifications/index.vue
@@ -1,20 +1,10 @@
 <script setup lang="ts">
 const { t } = useI18n()
-const masto = useMasto()
-
-// Default limit is 20 notifications, and servers are normally caped to 30
-const paginator = masto.notifications.iterate({ limit: 30 })
-
-const { clearNotifications } = useNotifications()
-onActivated(clearNotifications)
-
-const stream = await masto.stream.streamUser()
-
 useHeadFixed({
   title: () => `${t('tab.notifications_all')} | ${t('nav_side.notifications')}`,
 })
 </script>
 
 <template>
-  <NotificationPaginator v-bind="{ paginator, stream }" />
+  <TimelineNotifications v-if="isMastoInitialised" />
 </template>
diff --git a/pages/notifications/mention.vue b/pages/notifications/mention.vue
index 97a50781..a85c87c8 100644
--- a/pages/notifications/mention.vue
+++ b/pages/notifications/mention.vue
@@ -1,20 +1,10 @@
 <script setup lang="ts">
 const { t } = useI18n()
-
-const masto = useMasto()
-// Default limit is 20 notifications, and servers are normally caped to 30
-const paginator = masto.notifications.iterate({ limit: 30, types: ['mention'] })
-
-const { clearNotifications } = useNotifications()
-onActivated(clearNotifications)
-
-const stream = await masto.stream.streamUser()
-
 useHeadFixed({
   title: () => `${t('tab.notifications_mention')} | ${t('nav_side.notifications')}`,
 })
 </script>
 
 <template>
-  <NotificationPaginator v-bind="{ paginator, stream }" />
+  <TimelineNotifications v-if="isMastoInitialised" />
 </template>
diff --git a/pages/pinned.vue b/pages/pinned.vue
index 7094b55f..a88a16e6 100644
--- a/pages/pinned.vue
+++ b/pages/pinned.vue
@@ -18,6 +18,6 @@ useHeadFixed({
       <span>{{ t('account.pinned') }}</span>
     </template>
 
-    <TimelinePaginator :paginator="paginator" />
+    <TimelinePinned v-if="isMastoInitialised" />
   </MainContent>
 </template>
diff --git a/plugins/masto.ts b/plugins/masto.ts
index 1fd0ac5b..a3cecbed 100644
--- a/plugins/masto.ts
+++ b/plugins/masto.ts
@@ -2,7 +2,8 @@ export default defineNuxtPlugin(async (nuxtApp) => {
   const masto = createMasto()
 
   if (process.client) {
-    const { query } = useRoute()
+    const { query, path } = useRoute()
+    const router = useRouter()
     const user = typeof query.server === 'string' && typeof query.token === 'string'
       ? {
           server: query.server,
@@ -13,8 +14,13 @@ export default defineNuxtPlugin(async (nuxtApp) => {
 
     nuxtApp.hook('app:suspense:resolve', () => {
       // TODO: improve upstream to make this synchronous (delayed auth)
-      if (!masto.loggedIn.value)
-        masto.loginTo(user)
+      if (!masto.loggedIn.value) {
+        masto.loginTo(user).then(() => {
+          // This only cleans up the URL; page content should stay the same
+          if (path === '/signin/callback')
+            router.push('/home')
+        })
+      }
     })
   }