diff --git a/gulpfile.ts b/gulpfile.ts
index 21870473ed..736507bafb 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -56,7 +56,7 @@ gulp.task('build:js', () =>
 );
 
 gulp.task('build:ts', () => {
-	const tsProject = ts.createProject('./src/tsconfig.json');
+	const tsProject = ts.createProject('./tsconfig.json');
 
 	return tsProject
 		.src()
diff --git a/package.json b/package.json
index 33a0b2e2da..56501266b6 100644
--- a/package.json
+++ b/package.json
@@ -81,7 +81,6 @@
 		"accesses": "2.5.0",
 		"animejs": "2.2.0",
 		"autwh": "0.0.1",
-		"awesome-typescript-loader": "3.4.1",
 		"bcryptjs": "2.4.3",
 		"body-parser": "1.18.2",
 		"cafy": "3.2.1",
@@ -165,6 +164,7 @@
 		"tcp-port-used": "0.1.2",
 		"textarea-caret": "3.0.2",
 		"tmp": "0.0.33",
+		"ts-loader": "^3.5.0",
 		"ts-node": "4.1.0",
 		"tslint": "5.9.1",
 		"typescript": "2.7.1",
@@ -173,7 +173,9 @@
 		"uuid": "3.2.1",
 		"vhost": "3.0.2",
 		"vue": "^2.5.13",
+		"vue-loader": "^14.1.1",
 		"vue-router": "^3.0.1",
+		"vue-template-compiler": "^2.5.13",
 		"web-push": "3.2.5",
 		"webpack": "3.10.0",
 		"websocket": "1.0.25",
diff --git a/src/api/bot/core.ts b/src/api/bot/core.ts
index ddae6405f5..0a073a3127 100644
--- a/src/api/bot/core.ts
+++ b/src/api/bot/core.ts
@@ -305,7 +305,7 @@ class TlContext extends Context {
 	private async getTl() {
 		const tl = await require('../endpoints/posts/timeline')({
 			limit: 5,
-			max_id: this.next ? this.next : undefined
+			until_id: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (tl.length > 0) {
@@ -357,7 +357,7 @@ class NotificationsContext extends Context {
 	private async getNotifications() {
 		const notifications = await require('../endpoints/i/notifications')({
 			limit: 5,
-			max_id: this.next ? this.next : undefined
+			until_id: this.next ? this.next : undefined
 		}, this.bot.user);
 
 		if (notifications.length > 0) {
diff --git a/src/common/build/i18n.ts b/src/common/build/i18n.ts
index 1ae22147c4..500b8814fd 100644
--- a/src/common/build/i18n.ts
+++ b/src/common/build/i18n.ts
@@ -17,12 +17,19 @@ export default class Replacer {
 	}
 
 	private get(key: string) {
-		let text = locale[this.lang];
+		const texts = locale[this.lang];
+
+		if (texts == null) {
+			console.warn(`lang '${this.lang}' is not supported`);
+			return key; // Fallback
+		}
+
+		let text;
 
 		// Check the key existance
 		const error = key.split('.').some(k => {
-			if (text.hasOwnProperty(k)) {
-				text = text[k];
+			if (texts.hasOwnProperty(k)) {
+				text = texts[k];
 				return false;
 			} else {
 				return true;
diff --git a/src/web/app/desktop/mixins/index.ts b/src/web/app/desktop/mixins/index.ts
deleted file mode 100644
index e0c94ec5ee..0000000000
--- a/src/web/app/desktop/mixins/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-require('./user-preview');
-require('./widget');
diff --git a/src/web/app/desktop/mixins/user-preview.ts b/src/web/app/desktop/mixins/user-preview.ts
deleted file mode 100644
index 614de72bea..0000000000
--- a/src/web/app/desktop/mixins/user-preview.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as riot from 'riot';
-
-riot.mixin('user-preview', {
-	init: function() {
-		const scan = () => {
-			this.root.querySelectorAll('[data-user-preview]:not([data-user-preview-attached])')
-				.forEach(attach.bind(this));
-		};
-		this.on('mount', scan);
-		this.on('updated', scan);
-	}
-});
-
-function attach(el) {
-	el.setAttribute('data-user-preview-attached', true);
-
-	const user = el.getAttribute('data-user-preview');
-	let tag = null;
-	let showTimer = null;
-	let hideTimer = null;
-
-	el.addEventListener('mouseover', () => {
-		clearTimeout(showTimer);
-		clearTimeout(hideTimer);
-		showTimer = setTimeout(show, 500);
-	});
-
-	el.addEventListener('mouseleave', () => {
-		clearTimeout(showTimer);
-		clearTimeout(hideTimer);
-		hideTimer = setTimeout(close, 500);
-	});
-
-	this.on('unmount', () => {
-		clearTimeout(showTimer);
-		clearTimeout(hideTimer);
-		close();
-	});
-
-	const show = () => {
-		if (tag) return;
-		const preview = document.createElement('mk-user-preview');
-		const rect = el.getBoundingClientRect();
-		const x = rect.left + el.offsetWidth + window.pageXOffset;
-		const y = rect.top + window.pageYOffset;
-		preview.style.top = y + 'px';
-		preview.style.left = x + 'px';
-		preview.addEventListener('mouseover', () => {
-			clearTimeout(hideTimer);
-		});
-		preview.addEventListener('mouseleave', () => {
-			clearTimeout(showTimer);
-			hideTimer = setTimeout(close, 500);
-		});
-		tag = (riot as any).mount(document.body.appendChild(preview), {
-			user: user
-		})[0];
-	};
-
-	const close = () => {
-		if (tag) {
-			tag.close();
-			tag = null;
-		}
-	};
-}
diff --git a/src/web/app/desktop/mixins/widget.ts b/src/web/app/desktop/mixins/widget.ts
deleted file mode 100644
index 04131cd8f0..0000000000
--- a/src/web/app/desktop/mixins/widget.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as riot from 'riot';
-
-// ミックスインにオプションを渡せないのアレ
-// SEE: https://github.com/riot/riot/issues/2434
-
-(riot as any).mixin('widget', {
-	init: function() {
-		this.mixin('i');
-		this.mixin('api');
-
-		this.id = this.opts.id;
-		this.place = this.opts.place;
-
-		if (this.data) {
-			Object.keys(this.data).forEach(prop => {
-				this.data[prop] = this.opts.data.hasOwnProperty(prop) ? this.opts.data[prop] : this.data[prop];
-			});
-		}
-	},
-
-	save: function() {
-		this.update();
-		this.api('i/update_home', {
-			id: this.id,
-			data: this.data
-		}).then(() => {
-			this.I.client_settings.home.find(w => w.id == this.id).data = this.data;
-			this.I.update();
-		});
-	}
-});
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index 2d3714d845..4aef69b077 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -7,12 +7,13 @@ import './style.styl';
 
 import Vue from 'vue';
 import init from '../init';
-import route from './router';
 import fuckAdBlock from './scripts/fuck-ad-block';
 import MiOS from '../common/mios';
 import HomeStreamManager from '../common/scripts/streaming/home-stream-manager';
 import composeNotification from '../common/scripts/compose-notification';
 
+import MkIndex from './tags/pages/index.vue';
+
 /**
  * init
  */
@@ -36,8 +37,9 @@ init(async (mios: MiOS, app: Vue) => {
 		}
 	}
 
-	// Start routing
-	route(mios);
+	app.$router.addRoutes([{
+		path: '/', component: MkIndex, props: { os: mios }
+	}]);
 }, true);
 
 function registerNotifications(stream: HomeStreamManager) {
@@ -96,9 +98,9 @@ function registerNotifications(stream: HomeStreamManager) {
 			});
 			n.onclick = () => {
 				n.close();
-				(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
+				/*(riot as any).mount(document.body.appendChild(document.createElement('mk-messaging-room-window')), {
 					user: message.user
-				});
+				});*/
 			};
 			setTimeout(n.close.bind(n), 7000);
 		});
diff --git a/src/web/app/desktop/scripts/autocomplete.ts b/src/web/app/desktop/scripts/autocomplete.ts
index 9df7aae08d..8f075efdd1 100644
--- a/src/web/app/desktop/scripts/autocomplete.ts
+++ b/src/web/app/desktop/scripts/autocomplete.ts
@@ -1,4 +1,4 @@
-import getCaretCoordinates = require('textarea-caret');
+import getCaretCoordinates from 'textarea-caret';
 import * as riot from 'riot';
 
 /**
diff --git a/src/web/app/desktop/tags/pages/index.vue b/src/web/app/desktop/tags/pages/index.vue
new file mode 100644
index 0000000000..6bd036fc22
--- /dev/null
+++ b/src/web/app/desktop/tags/pages/index.vue
@@ -0,0 +1,3 @@
+<template>
+	<h1>hi</h1>
+</template>
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 5fb6ae7908..f0c36f6c12 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -5,7 +5,7 @@
 declare const _VERSION_: string;
 declare const _LANG_: string;
 declare const _HOST_: string;
-declare const __CONSTS__: any;
+//declare const __CONSTS__: any;
 
 import Vue from 'vue';
 import VueRouter from 'vue-router';
diff --git a/src/tsconfig.json b/src/web/app/tsconfig.json
similarity index 91%
rename from src/tsconfig.json
rename to src/web/app/tsconfig.json
index d88432d243..e31b52dab1 100644
--- a/src/tsconfig.json
+++ b/src/web/app/tsconfig.json
@@ -19,8 +19,5 @@
   "compileOnSave": false,
   "include": [
     "./**/*.ts"
-  ],
-  "exclude": [
-    "./web/app/**/*.ts"
   ]
 }
diff --git a/src/web/app/v.d.ts b/src/web/app/v.d.ts
new file mode 100644
index 0000000000..8f3a240d80
--- /dev/null
+++ b/src/web/app/v.d.ts
@@ -0,0 +1,4 @@
+declare module "*.vue" {
+	import Vue from 'vue';
+	export default Vue;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 68f6809b99..9d26429c51 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,6 +18,9 @@
   },
   "compileOnSave": false,
   "include": [
-    "./gulpfile.ts"
+    "./src/**/*.ts"
+  ],
+  "exclude": [
+    "./src/web/app/**/*.ts"
   ]
 }
diff --git a/webpack/module/rules/index.ts b/webpack/module/rules/index.ts
index b02bdef723..093f07330b 100644
--- a/webpack/module/rules/index.ts
+++ b/webpack/module/rules/index.ts
@@ -3,7 +3,7 @@ import license from './license';
 import fa from './fa';
 import base64 from './base64';
 import themeColor from './theme-color';
-import tag from './tag';
+import vue from './vue';
 import stylus from './stylus';
 import typescript from './typescript';
 
@@ -13,7 +13,7 @@ export default lang => [
 	fa(),
 	base64(),
 	themeColor(),
-	tag(),
+	vue(),
 	stylus(),
 	typescript()
 ];
diff --git a/webpack/module/rules/tag.ts b/webpack/module/rules/tag.ts
deleted file mode 100644
index 706af35b40..0000000000
--- a/webpack/module/rules/tag.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Riot tags
- */
-
-export default () => ({
-	test: /\.tag$/,
-	exclude: /node_modules/,
-	loader: 'riot-tag-loader',
-	query: {
-		hot: false,
-		style: 'stylus',
-		expr: false,
-		compact: true,
-		parserOptions: {
-			style: {
-				compress: true
-			}
-		}
-	}
-});
diff --git a/webpack/module/rules/typescript.ts b/webpack/module/rules/typescript.ts
index eb2b279a55..2c94137318 100644
--- a/webpack/module/rules/typescript.ts
+++ b/webpack/module/rules/typescript.ts
@@ -4,5 +4,9 @@
 
 export default () => ({
 	test: /\.ts$/,
-	use: 'awesome-typescript-loader'
+	loader: 'ts-loader',
+	options: {
+		configFile: __dirname + '/../../../src/web/app/tsconfig.json',
+		appendTsSuffixTo: [/\.vue$/]
+	}
 });
diff --git a/webpack/module/rules/vue.ts b/webpack/module/rules/vue.ts
new file mode 100644
index 0000000000..0d38b4deb3
--- /dev/null
+++ b/webpack/module/rules/vue.ts
@@ -0,0 +1,9 @@
+/**
+ * Vue
+ */
+
+export default () => ({
+	test: /\.vue$/,
+	exclude: /node_modules/,
+	loader: 'vue-loader'
+});
diff --git a/webpack/webpack.config.ts b/webpack/webpack.config.ts
index d67b8ef774..4386de3db9 100644
--- a/webpack/webpack.config.ts
+++ b/webpack/webpack.config.ts
@@ -15,12 +15,12 @@ module.exports = Object.keys(langs).map(lang => {
 	// Entries
 	const entry = {
 		desktop: './src/web/app/desktop/script.ts',
-		mobile: './src/web/app/mobile/script.ts',
-		ch: './src/web/app/ch/script.ts',
-		stats: './src/web/app/stats/script.ts',
-		status: './src/web/app/status/script.ts',
-		dev: './src/web/app/dev/script.ts',
-		auth: './src/web/app/auth/script.ts',
+		//mobile: './src/web/app/mobile/script.ts',
+		//ch: './src/web/app/ch/script.ts',
+		//stats: './src/web/app/stats/script.ts',
+		//status: './src/web/app/status/script.ts',
+		//dev: './src/web/app/dev/script.ts',
+		//auth: './src/web/app/auth/script.ts',
 		sw: './src/web/app/sw.js'
 	};