diff --git a/components/account/AccountHoverWrapper.vue b/components/account/AccountHoverWrapper.vue
index ee9d6287..db52b3f0 100644
--- a/components/account/AccountHoverWrapper.vue
+++ b/components/account/AccountHoverWrapper.vue
@@ -1,17 +1,20 @@
 <script setup lang="ts">
 import type { Account } from 'masto'
 
-defineProps<{
-  account: Account
+const props = defineProps<{
+  account?: Account
+  handle?: string
   disabled?: boolean
 }>()
+
+const account = props.account || (props.handle ? useAccountByHandle(props.handle!) : undefined)
 </script>
 
 <template>
-  <VMenu v-if="!disabled" placement="bottom-start" :delay="{ show: 500, hide: 100 }">
+  <VMenu v-if="!disabled && account" placement="bottom-start" :delay="{ show: 500, hide: 100 }">
     <slot />
     <template #popper>
-      <AccountHoverCard :account="account" />
+      <AccountHoverCard v-if="account" :account="account" />
     </template>
   </VMenu>
   <slot v-else />
diff --git a/components/status/StatusReplyingTo.vue b/components/status/StatusReplyingTo.vue
index ae5fa301..455f541b 100644
--- a/components/status/StatusReplyingTo.vue
+++ b/components/status/StatusReplyingTo.vue
@@ -5,7 +5,7 @@ const { status } = defineProps<{
   status: Status
 }>()
 
-const account = asyncComputed(() => fetchAccount(status.inReplyToAccountId!))
+const account = useAccountById(status.inReplyToAccountId!)
 </script>
 
 <template>
diff --git a/composables/cache.ts b/composables/cache.ts
index 5b7cbbb5..37d94af7 100644
--- a/composables/cache.ts
+++ b/composables/cache.ts
@@ -28,7 +28,7 @@ export function fetchStatus(id: string): Promise<Status> {
   return promise
 }
 
-export function fetchAccount(id: string): Promise<Account> {
+export function fetchAccountById(id: string): Promise<Account> {
   const key = `account:${id}`
   const cached = cache.get(key)
   if (cached)
@@ -42,7 +42,7 @@ export function fetchAccount(id: string): Promise<Account> {
   return promise
 }
 
-export async function fetchAccountByName(acct: string): Promise<Account> {
+export async function fetchAccountByHandle(acct: string): Promise<Account> {
   const key = `account:${acct}`
   const cached = cache.get(key)
   if (cached)
@@ -56,6 +56,14 @@ export async function fetchAccountByName(acct: string): Promise<Account> {
   return account
 }
 
+export function useAccountByHandle(acct: string) {
+  return useAsyncState(() => fetchAccountByHandle(acct), null).state
+}
+
+export function useAccountById(id: string) {
+  return useAsyncState(() => fetchAccountById(id), null).state
+}
+
 export function cacheStatus(status: Status, override?: boolean) {
   setCached(`status:${status.id}`, status, override)
 }
diff --git a/composables/content.ts b/composables/content.ts
index c1a32418..4e3e87c0 100644
--- a/composables/content.ts
+++ b/composables/content.ts
@@ -5,6 +5,7 @@ import type { VNode } from 'vue'
 import { Fragment, h, isVNode } from 'vue'
 import { RouterLink } from 'vue-router'
 import ContentCode from '~/components/content/ContentCode.vue'
+import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
 
 type Node = DefaultTreeAdapterMap['childNode']
 type Element = DefaultTreeAdapterMap['element']
@@ -18,7 +19,9 @@ function handleMention(el: Element) {
       if (matchUser) {
         const [, server, username] = matchUser
         // Handles need to ignore server subdomains
-        href.value = `/@${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
+        const handle = `@${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
+        href.value = `/${handle}`
+        return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el))
       }
       const matchTag = href.value.match(TagLinkRE)
       if (matchTag) {
@@ -108,22 +111,13 @@ export function contentToVNode(
   return h(Fragment, tree.childNodes.map(n => treeToVNode(n)))
 }
 
-function treeToVNode(
-  input: Node,
-): VNode | string | null {
-  if (input.nodeName === '#text') {
+function nodeToVNode(node: Node): VNode | string | null {
+  if (node.nodeName === '#text') {
     // @ts-expect-error casing
-    const text = input.value as string
-    return text
+    return input.value as string
   }
 
-  if ('childNodes' in input) {
-    const node = handleNode(input)
-    if (node == null)
-      return null
-    if (isVNode(node))
-      return node
-
+  if ('childNodes' in node) {
     const attrs = Object.fromEntries(node.attrs.map(i => [i.name, i.value]))
     if (node.nodeName === 'a' && (attrs.href?.startsWith('/') || attrs.href?.startsWith('.'))) {
       attrs.to = attrs.href
@@ -144,6 +138,25 @@ function treeToVNode(
   return null
 }
 
+function treeToVNode(
+  input: Node,
+): VNode | string | null {
+  if (input.nodeName === '#text') {
+    // @ts-expect-error casing
+    return input.value as string
+  }
+
+  if ('childNodes' in input) {
+    const node = handleNode(input)
+    if (node == null)
+      return null
+    if (isVNode(node))
+      return node
+    return nodeToVNode(node)
+  }
+  return null
+}
+
 export function htmlToText(html: string) {
   const tree = parseFragment(html)
   return tree.childNodes.map(n => treeToText(n)).join('').trim()
diff --git a/pages/@[account]/index/followers.vue b/pages/@[account]/index/followers.vue
index 5b7ef1ed..a7109fb2 100644
--- a/pages/@[account]/index/followers.vue
+++ b/pages/@[account]/index/followers.vue
@@ -1,8 +1,8 @@
 <script setup lang="ts">
 const params = useRoute().params
-const accountName = $(computedEager(() => params.account as string))
+const handle = $(computedEager(() => params.account as string))
 
-const account = await fetchAccountByName(accountName)
+const account = await fetchAccountByHandle(handle)
 const paginator = account ? useMasto().accounts.getFollowersIterable(account.id, {}) : null
 </script>
 
diff --git a/pages/@[account]/index/following.vue b/pages/@[account]/index/following.vue
index f6dff0dc..d08e6cae 100644
--- a/pages/@[account]/index/following.vue
+++ b/pages/@[account]/index/following.vue
@@ -1,8 +1,8 @@
 <script setup lang="ts">
 const params = useRoute().params
-const accountName = $(computedEager(() => params.account as string))
+const handle = $(computedEager(() => params.account as string))
 
-const account = await fetchAccountByName(accountName)
+const account = await fetchAccountByHandle(handle)
 const paginator = account ? useMasto().accounts.getFollowingIterable(account.id, {}) : null
 </script>
 
diff --git a/pages/@[account]/index/index.vue b/pages/@[account]/index/index.vue
index e2790100..0b7de973 100644
--- a/pages/@[account]/index/index.vue
+++ b/pages/@[account]/index/index.vue
@@ -1,8 +1,8 @@
 <script setup lang="ts">
 const params = useRoute().params
-const accountName = $(computedEager(() => params.account as string))
+const handle = $(computedEager(() => params.account as string))
 
-const account = await fetchAccountByName(accountName)
+const account = await fetchAccountByHandle(handle)
 const { t } = useI18n()
 
 const paginatorPosts = useMasto().accounts.getStatusesIterable(account.id, { excludeReplies: true })
diff --git a/tests/content-rich.test.ts b/tests/content-rich.test.ts
index ae475044..7274c211 100644
--- a/tests/content-rich.test.ts
+++ b/tests/content-rich.test.ts
@@ -91,3 +91,13 @@ vi.mock('../components/content/ContentCode.vue', () => {
     }),
   }
 })
+
+vi.mock('../components/account/AccountHoverWrapper.vue', () => {
+  return {
+    default: defineComponent({
+      setup(_, { slots }) {
+        return () => slots?.default?.()
+      },
+    }),
+  }
+})