diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 8c10bdee28..b58ba37ecb 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -4,7 +4,7 @@ import signin from './signin.vue';
 import signup from './signup.vue';
 import forkit from './forkit.vue';
 import nav from './nav.vue';
-import postHtml from './post-html.vue';
+import postHtml from './post-html';
 import poll from './poll.vue';
 import pollEditor from './poll-editor.vue';
 import reactionIcon from './reaction-icon.vue';
diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue
index 25ceab85a1..91af26bffe 100644
--- a/src/client/app/common/views/components/messaging-room.message.vue
+++ b/src/client/app/common/views/components/messaging-room.message.vue
@@ -4,13 +4,13 @@
 		<img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/>
 	</router-link>
 	<div class="content">
-		<div class="balloon" :data-no-text="message.textHtml == null">
+		<div class="balloon" :data-no-text="message.text == null">
 			<p class="read" v-if="isMe && message.isRead">%i18n:common.tags.mk-messaging-message.is-read%</p>
 			<button class="delete-button" v-if="isMe" title="%i18n:common.delete%">
 				<img src="/assets/desktop/messaging/delete.png" alt="Delete"/>
 			</button>
 			<div class="content" v-if="!message.isDeleted">
-				<mk-post-html class="text" v-if="message.textHtml" ref="text" :html="message.textHtml" :i="os.i"/>
+				<mk-post-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/>
 				<div class="file" v-if="message.file">
 					<a :href="message.file.url" target="_blank" :title="message.file.name">
 						<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
@@ -35,35 +35,30 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
 
 export default Vue.extend({
-	props: ['message'],
-	data() {
-		return {
-			urls: []
-		};
+	props: {
+		message: {
+			required: true
+		}
 	},
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.message.user);
 		},
 		isMe(): boolean {
 			return this.message.userId == (this as any).os.i.id;
-		}
-	},
-	watch: {
-		message: {
-			handler(newMessage, oldMessage) {
-				if (!oldMessage || newMessage.textHtml !== oldMessage.textHtml) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
-
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
+		},
+		urls(): string[] {
+			if (this.message.text) {
+				const ast = parse(this.message.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	}
 });
diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts
new file mode 100644
index 0000000000..c5c3b72758
--- /dev/null
+++ b/src/client/app/common/views/components/post-html.ts
@@ -0,0 +1,157 @@
+import Vue from 'vue';
+import * as emojilib from 'emojilib';
+import parse from '../../../../../common/text/parse';
+import getAcct from '../../../../../common/user/get-acct';
+import { url } from '../../../config';
+import MkUrl from './url.vue';
+
+const flatten = list => list.reduce(
+	(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
+);
+
+export default Vue.component('mk-post-html', {
+	props: {
+		text: {
+			type: String,
+			required: true
+		},
+		ast: {
+			type: [],
+			required: false
+		},
+		shouldBreak: {
+			type: Boolean,
+			default: true
+		},
+		i: {
+			type: Object,
+			default: null
+		}
+	},
+
+	render(createElement) {
+		let ast;
+
+		if (this.ast == null) {
+			// Parse text to ast
+			ast = parse(this.text);
+		} else {
+			ast = this.ast;
+		}
+
+		// Parse ast to DOM
+		const els = flatten(ast.map(token => {
+			switch (token.type) {
+				case 'text':
+					const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if (this.shouldBreak) {
+						const x = text.split('\n')
+							.map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]);
+						x[x.length - 1].pop();
+						return x;
+					} else {
+						return createElement('span', text.replace(/\n/g, ' '));
+					}
+
+				case 'bold':
+					return createElement('strong', token.bold);
+
+				case 'url':
+					return createElement(MkUrl, {
+						props: {
+							url: token.content,
+							target: '_blank'
+						}
+					});
+
+				case 'link':
+					return createElement('a', {
+						attrs: {
+							class: 'link',
+							href: token.url,
+							target: '_blank',
+							title: token.url
+						}
+					}, token.title);
+
+				case 'mention':
+					return (createElement as any)('a', {
+						attrs: {
+							href: `${url}/@${getAcct(token)}`,
+							target: '_blank',
+							dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token)
+						},
+						directives: [{
+							name: 'user-preview',
+							value: token.content
+						}]
+					}, token.content);
+
+				case 'hashtag':
+					return createElement('a', {
+						attrs: {
+							href: `${url}/search?q=${token.content}`,
+							target: '_blank'
+						}
+					}, token.content);
+
+				case 'code':
+					return createElement('pre', [
+						createElement('code', {
+							domProps: {
+								innerHTML: token.html
+							}
+						})
+					]);
+
+				case 'inline-code':
+					return createElement('code', {
+						domProps: {
+							innerHTML: token.html
+						}
+					});
+
+				case 'quote':
+					const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
+
+					if (this.shouldBreak) {
+						const x = text2.split('\n')
+							.map(t => [createElement('span', t), createElement('br')]);
+						x[x.length - 1].pop();
+						return createElement('div', {
+							attrs: {
+								class: 'quote'
+							}
+						}, x);
+					} else {
+						return createElement('span', {
+							attrs: {
+								class: 'quote'
+							}
+						}, text2.replace(/\n/g, ' '));
+					}
+
+				case 'emoji':
+					const emoji = emojilib.lib[token.emoji];
+					return createElement('span', emoji ? emoji.char : token.content);
+
+				default:
+					console.log('unknown ast type:', token.type);
+			}
+		}));
+
+		const _els = [];
+		els.forEach((el, i) => {
+			if (el.tag == 'br') {
+				if (els[i - 1].tag != 'div') {
+					_els.push(el);
+				}
+			} else {
+				_els.push(el);
+			}
+		});
+
+		return createElement('span', _els);
+	}
+});
diff --git a/src/client/app/common/views/components/post-html.vue b/src/client/app/common/views/components/post-html.vue
deleted file mode 100644
index 1c949052b9..0000000000
--- a/src/client/app/common/views/components/post-html.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template><div class="mk-post-html" v-html="html"></div></template>
-
-<script lang="ts">
-import Vue from 'vue';
-import getAcct from '../../../../../common/user/get-acct';
-import { url } from '../../../config';
-
-function markUrl(a) {
-	while (a.firstChild) {
-		a.removeChild(a.firstChild);
-	}
-
-	const schema = document.createElement('span');
-	const delimiter = document.createTextNode('//');
-	const host = document.createElement('span');
-	const pathname = document.createElement('span');
-	const query = document.createElement('span');
-	const hash = document.createElement('span');
-
-	schema.className = 'schema';
-	schema.textContent = a.protocol;
-
-	host.className = 'host';
-	host.textContent = a.host;
-
-	pathname.className = 'pathname';
-	pathname.textContent = a.pathname;
-
-	query.className = 'query';
-	query.textContent = a.search;
-
-	hash.className = 'hash';
-	hash.textContent = a.hash;
-
-	a.appendChild(schema);
-	a.appendChild(delimiter);
-	a.appendChild(host);
-	a.appendChild(pathname);
-	a.appendChild(query);
-	a.appendChild(hash);
-}
-
-function markMe(me, a) {
-	a.setAttribute("data-is-me", me && `${url}/@${getAcct(me)}` == a.href);
-}
-
-function markTarget(a) {
-	a.setAttribute("target", "_blank");
-}
-
-export default Vue.component('mk-post-html', {
-	props: {
-		html: {
-			type: String,
-			required: true
-		},
-		i: {
-			type: Object,
-			default: null
-		}
-	},
-	watch {
-		html: {
-			handler() {
-				this.$nextTick(() => [].forEach.call(this.$el.getElementsByTagName('a'), a => {
-					if (a.href === a.textContent) {
-						markUrl(a);
-					} else {
-						markMe((this as any).i, a);
-					}
-
-					markTarget(a);
-				}));
-			},
-			immediate: true,
-		}
-	}
-});
-</script>
-
-<style lang="stylus">
-.mk-post-html
-	a
-		word-break break-all
-
-		> .schema
-			opacity 0.5
-
-		> .host
-			font-weight bold
-
-		> .pathname
-			opacity 0.8
-
-		> .query
-			opacity 0.5
-
-		> .hash
-			font-style italic
-
-	p
-		margin 0
-</style>
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
new file mode 100644
index 0000000000..e6ffe4466d
--- /dev/null
+++ b/src/client/app/common/views/components/url.vue
@@ -0,0 +1,57 @@
+<template>
+<a class="mk-url" :href="url" :target="target">
+	<span class="schema">{{ schema }}//</span>
+	<span class="hostname">{{ hostname }}</span>
+	<span class="port" v-if="port != ''">:{{ port }}</span>
+	<span class="pathname" v-if="pathname != ''">{{ pathname }}</span>
+	<span class="query">{{ query }}</span>
+	<span class="hash">{{ hash }}</span>
+	%fa:external-link-square-alt%
+</a>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['url', 'target'],
+	data() {
+		return {
+			schema: null,
+			hostname: null,
+			port: null,
+			pathname: null,
+			query: null,
+			hash: null
+		};
+	},
+	created() {
+		const url = new URL(this.url);
+		this.schema = url.protocol;
+		this.hostname = url.hostname;
+		this.port = url.port;
+		this.pathname = url.pathname;
+		this.query = url.search;
+		this.hash = url.hash;
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-url
+	word-break break-all
+	> [data-fa]
+		padding-left 2px
+		font-size .9em
+		font-weight 400
+		font-style normal
+	> .schema
+		opacity 0.5
+	> .hostname
+		font-weight bold
+	> .pathname
+		opacity 0.8
+	> .query
+		opacity 0.5
+	> .hash
+		font-style italic
+</style>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index f379029f9f..09b090bdc1 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -15,7 +15,7 @@
 				</div>
 			</header>
 			<div class="text">
-				<mk-post-html :html="post.textHtml"/>
+				<mk-post-html :text="post.text"/>
 			</div>
 		</div>
 	</div>
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
index b6148d9b28..1d5649cf92 100644
--- a/src/client/app/desktop/views/components/post-detail.sub.vue
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -16,7 +16,7 @@
 			</div>
 		</header>
 		<div class="body">
-			<mk-post-html v-if="post.textHtml" :html="post.textHtml" :i="os.i" :class="$style.text"/>
+			<mk-post-html v-if="post.text" :text="post.text" :i="os.i" :class="$style.text"/>
 			<div class="media" v-if="post.media > 0">
 				<mk-media-list :media-list="post.media"/>
 			</div>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
index e75ebe34b4..70bfdbba35 100644
--- a/src/client/app/desktop/views/components/post-detail.vue
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -38,7 +38,7 @@
 			</router-link>
 		</header>
 		<div class="body">
-			<mk-post-html :class="$style.text" v-if="p.text" ref="text" :text="p.text" :i="os.i"/>
+			<mk-post-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
 			<div class="media" v-if="p.media.length > 0">
 				<mk-media-list :media-list="p.media"/>
 			</div>
@@ -79,6 +79,7 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
@@ -90,6 +91,7 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: {
 		post: {
 			type: Object,
@@ -99,19 +101,15 @@ export default Vue.extend({
 			default: false
 		}
 	},
-	computed: {
-		acct() {
-			return getAcct(this.post.user);
-		}
-	},
+
 	data() {
 		return {
 			context: [],
 			contextFetching: false,
-			replies: [],
-			urls: []
+			replies: []
 		};
 	},
+
 	computed: {
 		isRepost(): boolean {
 			return (this.post.repost &&
@@ -131,8 +129,22 @@ export default Vue.extend({
 		},
 		title(): string {
 			return dateStringify(this.p.createdAt);
+		},
+		acct(): string {
+			return getAcct(this.p.user);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	mounted() {
 		// Get replies
 		if (!this.compact) {
@@ -162,21 +174,7 @@ export default Vue.extend({
 			}
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.text !== oldPost.text) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		fetchContext() {
 			this.contextFetching = true;
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
index f3566c81bf..c31e28d67f 100644
--- a/src/client/app/desktop/views/components/posts.post.vue
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -38,7 +38,7 @@
 				</p>
 				<div class="text">
 					<a class="reply" v-if="p.reply">%fa:reply%</a>
-					<mk-post-html v-if="p.textHtml" ref="text" :html="p.textHtml" :i="os.i" :class="$style.text"/>
+					<mk-post-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
@@ -86,6 +86,8 @@
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
+
 import MkPostFormWindow from './post-form-window.vue';
 import MkRepostFormWindow from './repost-form-window.vue';
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
@@ -107,17 +109,19 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: ['post'],
+
 	data() {
 		return {
 			isDetailOpened: false,
 			connection: null,
-			connectionId: null,
-			urls: []
+			connectionId: null
 		};
 	},
+
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.p.user);
 		},
 		isRepost(): boolean {
@@ -141,14 +145,26 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/@${this.acct}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	created() {
 		if ((this as any).os.isSignedIn) {
 			this.connection = (this as any).os.stream.getConnection();
 			this.connectionId = (this as any).os.stream.use();
 		}
 	},
+
 	mounted() {
 		this.capture(true);
 
@@ -174,6 +190,7 @@ export default Vue.extend({
 			}
 		}
 	},
+
 	beforeDestroy() {
 		this.decapture(true);
 
@@ -182,21 +199,7 @@ export default Vue.extend({
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.textHtml !== oldPost.textHtml) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
@@ -457,7 +460,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					>>> blockquote
+					>>> .quote
 						margin 8px
 						padding 6px 12px
 						color #aaa
diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue
index 58c81e7552..17899af280 100644
--- a/src/client/app/desktop/views/components/sub-post-content.vue
+++ b/src/client/app/desktop/views/components/sub-post-content.vue
@@ -2,7 +2,7 @@
 <div class="mk-sub-post-content">
 	<div class="body">
 		<a class="reply" v-if="post.replyId">%fa:reply%</a>
-		<mk-post-html ref="text" :html="post.textHtml" :i="os.i"/>
+		<mk-post-html :text="post.text" :i="os.i"/>
 		<a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a>
 	</div>
 	<details v-if="post.media.length > 0">
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
index 77a73426f2..0a4e36fc60 100644
--- a/src/client/app/mobile/views/components/post-detail.vue
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -81,6 +81,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
+
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './post-detail.sub.vue';
@@ -89,6 +91,7 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: {
 		post: {
 			type: Object,
@@ -98,19 +101,20 @@ export default Vue.extend({
 			default: false
 		}
 	},
+
 	data() {
 		return {
 			context: [],
 			contextFetching: false,
-			replies: [],
-			urls: []
+			replies: []
 		};
 	},
+
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.post.user);
 		},
-		pAcct() {
+		pAcct(): string {
 			return getAcct(this.p.user);
 		},
 		isRepost(): boolean {
@@ -128,8 +132,19 @@ export default Vue.extend({
 					.map(key => this.p.reactionCounts[key])
 					.reduce((a, b) => a + b)
 				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	mounted() {
 		// Get replies
 		if (!this.compact) {
@@ -159,21 +174,7 @@ export default Vue.extend({
 			}
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.text !== oldPost.text) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		fetchContext() {
 			this.contextFetching = true;
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
index 96ec9632f1..f4f845b49a 100644
--- a/src/client/app/mobile/views/components/post.vue
+++ b/src/client/app/mobile/views/components/post.vue
@@ -37,7 +37,7 @@
 					<a class="reply" v-if="p.reply">
 						%fa:reply%
 					</a>
-					<mk-post-html v-if="p.text" ref="text" :text="p.text" :i="os.i" :class="$style.text"/>
+					<mk-post-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
 					<a class="rp" v-if="p.repost != null">RP:</a>
 				</div>
 				<div class="media" v-if="p.media.length > 0">
@@ -78,6 +78,8 @@
 <script lang="ts">
 import Vue from 'vue';
 import getAcct from '../../../../../common/user/get-acct';
+import parse from '../../../../../common/text/parse';
+
 import MkPostMenu from '../../../common/views/components/post-menu.vue';
 import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
 import XSub from './post.sub.vue';
@@ -86,19 +88,21 @@ export default Vue.extend({
 	components: {
 		XSub
 	},
+
 	props: ['post'],
+
 	data() {
 		return {
 			connection: null,
-			connectionId: null,
-			urls: []
+			connectionId: null
 		};
 	},
+
 	computed: {
-		acct() {
+		acct(): string {
 			return getAcct(this.post.user);
 		},
-		pAcct() {
+		pAcct(): string {
 			return getAcct(this.p.user);
 		},
 		isRepost(): boolean {
@@ -119,14 +123,26 @@ export default Vue.extend({
 		},
 		url(): string {
 			return `/@${this.pAcct}/${this.p.id}`;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
 		}
 	},
+
 	created() {
 		if ((this as any).os.isSignedIn) {
 			this.connection = (this as any).os.stream.getConnection();
 			this.connectionId = (this as any).os.stream.use();
 		}
 	},
+
 	mounted() {
 		this.capture(true);
 
@@ -152,6 +168,7 @@ export default Vue.extend({
 			}
 		}
 	},
+
 	beforeDestroy() {
 		this.decapture(true);
 
@@ -160,21 +177,7 @@ export default Vue.extend({
 			(this as any).os.stream.dispose(this.connectionId);
 		}
 	},
-	watch: {
-		post: {
-			handler(newPost, oldPost) {
-				if (!oldPost || newPost.text !== oldPost.text) {
-					this.$nextTick(() => {
-						const elements = this.$refs.text.$el.getElementsByTagName('a');
 
-						this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin)
-							.map(({ href }) => href);
-					});
-				}
-			},
-			immediate: true
-		}
-	},
 	methods: {
 		capture(withHandler = false) {
 			if ((this as any).os.isSignedIn) {
@@ -396,7 +399,7 @@ export default Vue.extend({
 					font-size 1.1em
 					color #717171
 
-					>>> blockquote
+					>>> .quote
 						margin 8px
 						padding 6px 12px
 						color #aaa
diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue
index 955bb406b4..97dd987dd7 100644
--- a/src/client/app/mobile/views/components/sub-post-content.vue
+++ b/src/client/app/mobile/views/components/sub-post-content.vue
@@ -2,7 +2,7 @@
 <div class="mk-sub-post-content">
 	<div class="body">
 		<a class="reply" v-if="post.replyId">%fa:reply%</a>
-		<mk-post-html v-if="post.text" :ast="post.text" :i="os.i"/>
+		<mk-post-html v-if="post.text" :text="post.text" :i="os.i"/>
 		<a class="rp" v-if="post.repostId">RP: ...</a>
 	</div>
 	<details v-if="post.media.length > 0">
diff --git a/src/common/text/parse/index.ts b/src/common/text/parse/index.ts
index 1e2398dc38..b958da81b0 100644
--- a/src/common/text/parse/index.ts
+++ b/src/common/text/parse/index.ts
@@ -14,7 +14,7 @@ const elements = [
 	require('./elements/emoji')
 ];
 
-export default (source: string) => {
+export default (source: string): any[] => {
 
 	if (source == '') {
 		return null;