diff --git a/package.json b/package.json
index 53d252ec..d4c4e580 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "@pinia/nuxt": "^0.4.5",
     "@types/fs-extra": "^9.0.13",
     "@types/js-yaml": "^4.0.5",
+    "@types/prettier": "^2.7.1",
     "@types/sanitize-html": "^2.6.2",
     "@types/wicg-file-system-access": "^2020.9.5",
     "@unocss/nuxt": "^0.46.5",
@@ -45,12 +46,14 @@
     "parse5": "^7.1.2",
     "pinia": "^2.0.26",
     "postcss-nested": "^6.0.0",
+    "prettier": "^2.8.0",
     "rollup-plugin-node-polyfills": "^0.2.1",
     "sanitize-html": "^2.7.3",
     "shiki": "^0.11.1",
     "theme-vitesse": "^0.6.0",
     "typescript": "^4.9.3",
     "ufo": "^1.0.0",
+    "unplugin-auto-import": "^0.11.5",
     "vitest": "^0.25.3"
   }
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7e875ff5..2ba8c630 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10,6 +10,7 @@ specifiers:
   '@pinia/nuxt': ^0.4.5
   '@types/fs-extra': ^9.0.13
   '@types/js-yaml': ^4.0.5
+  '@types/prettier': ^2.7.1
   '@types/sanitize-html': ^2.6.2
   '@types/wicg-file-system-access': ^2020.9.5
   '@unocss/nuxt': ^0.46.5
@@ -30,12 +31,14 @@ specifiers:
   parse5: ^7.1.2
   pinia: ^2.0.26
   postcss-nested: ^6.0.0
+  prettier: ^2.8.0
   rollup-plugin-node-polyfills: ^0.2.1
   sanitize-html: ^2.7.3
   shiki: ^0.11.1
   theme-vitesse: ^0.6.0
   typescript: ^4.9.3
   ufo: ^1.0.0
+  unplugin-auto-import: ^0.11.5
   vitest: ^0.25.3
 
 devDependencies:
@@ -48,6 +51,7 @@ devDependencies:
   '@pinia/nuxt': 0.4.5_typescript@4.9.3
   '@types/fs-extra': 9.0.13
   '@types/js-yaml': 4.0.5
+  '@types/prettier': 2.7.1
   '@types/sanitize-html': 2.6.2
   '@types/wicg-file-system-access': 2020.9.5
   '@unocss/nuxt': 0.46.5
@@ -68,12 +72,14 @@ devDependencies:
   parse5: 7.1.2
   pinia: 2.0.26_typescript@4.9.3
   postcss-nested: 6.0.0
+  prettier: 2.8.0
   rollup-plugin-node-polyfills: 0.2.1
   sanitize-html: 2.7.3
   shiki: 0.11.1
   theme-vitesse: 0.6.0
   typescript: 4.9.3
   ufo: 1.0.0
+  unplugin-auto-import: 0.11.5
   vitest: 0.25.3
 
 packages:
@@ -195,6 +201,10 @@ packages:
     resolution: {integrity: sha512-sEYpyyKUPOew9QsXZ8feRVMzW6DWLviwOl+/ap06UQW02A8Srbc95CPHVm4eUbiBzBgD46eyIT+przv//KSSlQ==}
     dev: true
 
+  /@antfu/utils/0.7.0:
+    resolution: {integrity: sha512-tH38JQEFLOdvZJC32ZbPTvWOQzxEtOQh5jOqBPDLw8sxBr0PFF+f2Csgwb7mRpD0QB1xu+PDoAifIPiCNneeNA==}
+    dev: true
+
   /@babel/code-frame/7.18.6:
     resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==}
     engines: {node: '>=6.9.0'}
@@ -1107,6 +1117,10 @@ packages:
     resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
     dev: true
 
+  /@types/prettier/2.7.1:
+    resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==}
+    dev: true
+
   /@types/resolve/1.20.2:
     resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
     dev: true
@@ -5968,6 +5982,12 @@ packages:
     engines: {node: '>= 0.8.0'}
     dev: true
 
+  /prettier/2.8.0:
+    resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==}
+    engines: {node: '>=10.13.0'}
+    hasBin: true
+    dev: true
+
   /pretty-bytes/6.0.0:
     resolution: {integrity: sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==}
     engines: {node: ^14.13.1 || >=16.0.0}
@@ -6973,6 +6993,25 @@ packages:
       - vite
     dev: true
 
+  /unplugin-auto-import/0.11.5:
+    resolution: {integrity: sha512-nvbL2AQwLRR8wbHpJ6L1EBVNmjN045RSedTa4NtsGRkSQFXkI1iKHs4dTqJwcKZsnFrZOAKtLPiN1/oQTObLZw==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@vueuse/core': '*'
+    peerDependenciesMeta:
+      '@vueuse/core':
+        optional: true
+    dependencies:
+      '@antfu/utils': 0.7.0
+      '@rollup/pluginutils': 5.0.2
+      local-pkg: 0.4.2
+      magic-string: 0.26.7
+      unimport: 1.0.1
+      unplugin: 1.0.0
+    transitivePeerDependencies:
+      - rollup
+    dev: true
+
   /unplugin-combine/0.2.8:
     resolution: {integrity: sha512-Z38AC/TEjXbVyZ5HjVqo+lADj0/dcfwWC0Z4y0LNhybJzJQwmcMxm+ZsqHY3faauj4YigmlRMdptR5JEW9RuLg==}
     engines: {node: '>=14.19.0'}
diff --git a/tests/__snapshots__/content.test.ts.snap b/tests/__snapshots__/content.test.ts.snap
index 549d3f8a..c2e4059e 100644
--- a/tests/__snapshots__/content.test.ts.snap
+++ b/tests/__snapshots__/content.test.ts.snap
@@ -1,6 +1,49 @@
 // Vitest Snapshot v1
 
-exports[`rich-content > plain 1`] = `
-"Hello
-World"
+exports[`rich-content > code frame 1`] = `
+"<p>Testing code block</p>
+<pre lang=\\"ts\\">
+import { useMouse, usePreferredDark } from &#39;@vueuse/core&#39;
+// tracks mouse position
+const { x, y } = useMouse()
+// is the user prefers dark theme
+const isDark = usePreferredDark()</pre
+>
+<p></p>
+"
+`;
+
+exports[`rich-content > custom emoji 1`] = `
+"Daniel Roe
+<img
+  src=\\"https://media.mas.to/masto-public/cache/custom_emojis/images/000/288/667/original/c96ba3cb0e0e1eac.png\\"
+  alt=\\"nuxt\\"
+  class=\\"custom-emoji\\"
+/>
+"
+`;
+
+exports[`rich-content > empty 1`] = `""`;
+
+exports[`rich-content > link + mention 1`] = `
+"<p>
+  Happy 🤗 we’re now using
+  <span class=\\"h-card\\"
+    ><a
+      class=\\"u-url mention\\"
+      rel=\\"nofollow noopener noreferrer\\"
+      to=\\"/@vitest@mas.to\\"
+    ></a
+  ></span>
+  (migrated from chai+mocha)
+  <a
+    href=\\"https://github.com/ayoayco/astro-reactive-library/pull/203\\"
+    rel=\\"nofollow noopener noreferrer\\"
+    target=\\"_blank\\"
+    ><span class=\\"invisible\\">https://</span
+    ><span class=\\"ellipsis\\">github.com/ayoayco/astro-react</span
+    ><span class=\\"invisible\\">ive-library/pull/203</span></a
+  >
+</p>
+"
 `;
diff --git a/tests/content.test.ts b/tests/content.test.ts
index 27f00df0..142563c6 100644
--- a/tests/content.test.ts
+++ b/tests/content.test.ts
@@ -1,21 +1,81 @@
 import type { Emoji } from 'masto'
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
 import { renderToString } from 'vue/server-renderer'
+import { format } from 'prettier'
 import { contentToVNode } from '~/composables/content'
 
+describe('rich-content', () => {
+  it('empty', async () => {
+    const { formatted } = await render('')
+    expect(formatted).toMatchSnapshot()
+  })
+
+  it('link + mention', async () => {
+    // https://fosstodon.org/@ayo/109383002937620723
+    const { formatted } = await render('<p>Happy 🤗 we’re now using <span class="h-card"><a href="https://mas.to/@vitest" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@<span>vitest</span></a></span> (migrated from chai+mocha) <a href="https://github.com/ayoayco/astro-reactive-library/pull/203" rel="nofollow noopener noreferrer" target="_blank"><span class="invisible">https://</span><span class="ellipsis">github.com/ayoayco/astro-react</span><span class="invisible">ive-library/pull/203</span></a></p>')
+    expect(formatted).toMatchSnapshot()
+  })
+
+  it('custom emoji', async () => {
+    const { formatted } = await render('Daniel Roe :nuxt:', {
+      nuxt: {
+        shortcode: 'nuxt',
+        url: 'https://media.mas.to/masto-public/cache/custom_emojis/images/000/288/667/original/c96ba3cb0e0e1eac.png',
+        staticUrl: 'https://media.mas.to/masto-public/cache/custom_emojis/images/000/288/667/static/c96ba3cb0e0e1eac.png',
+        visibleInPicker: true,
+      },
+    })
+    expect(formatted).toMatchSnapshot()
+  })
+
+  it('code frame', async () => {
+    // https://mas.to/@antfu/109396489827394721
+    const { formatted } = await render('<p>Testing code block</p><p>```ts<br />import { useMouse, usePreferredDark } from &#39;@vueuse/core&#39;</p><p>// tracks mouse position<br />const { x, y } = useMouse()</p><p>// is the user prefers dark theme<br />const isDark = usePreferredDark()<br />```</p>')
+    expect(formatted).toMatchSnapshot()
+  })
+})
+
 async function render(content: string, emojis?: Record<string, Emoji>) {
   const vnode = contentToVNode(content, emojis)
   const html = (await renderToString(vnode))
     .replace(/<!--[\[\]]-->/g, '')
+  const formatted = format(html, {
+    parser: 'html',
+  })
+
   return {
     vnode,
     html,
+    formatted,
   }
 }
 
-describe('rich-content', () => {
-  it('plain', async () => {
-    const { html } = await render('Hello\nWorld')
-    expect(html).toMatchSnapshot()
-  })
+// mocks
+vi.mock('vue-router', () => {
+  return {
+    RouterLink: defineComponent((attrs) => {
+      return () => h('a', attrs)
+    }),
+  }
+})
+
+vi.mock('../components/content/ContentCode.vue', () => {
+  return {
+    default: defineComponent({
+      props: {
+        code: {
+          type: String,
+          required: true,
+        },
+        lang: {
+          type: String,
+          required: true,
+        },
+      },
+      setup(props) {
+        const raw = computed(() => decodeURIComponent(props.code).replace(/&#39;/g, '\''))
+        return () => h('pre', { lang: props.lang }, raw.value)
+      },
+    }),
+  }
 })
diff --git a/vitest.config.ts b/vitest.config.ts
index f335e362..5cd8a6d4 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,6 +1,7 @@
 import { resolve } from 'path'
 import { defineConfig } from 'vitest/config'
 import Vue from '@vitejs/plugin-vue'
+import AutoImport from 'unplugin-auto-import/vite'
 
 export default defineConfig({
   resolve: {
@@ -10,5 +11,15 @@ export default defineConfig({
   },
   plugins: [
     Vue(),
+    AutoImport({
+      dts: false,
+      imports: [
+        'vue',
+        '@vueuse/core',
+      ],
+      dirs: [
+        'composables',
+      ],
+    }),
   ],
 })