diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js
new file mode 100644
index 0000000000..81f497b5b8
--- /dev/null
+++ b/cypress/e2e/router.cy.js
@@ -0,0 +1,30 @@
+describe('Router transition', () => {
+	describe('Redirect', () => {
+		// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
+		before(() => {
+			cy.resetState();
+
+			// インスタンス初期セットアップ
+			cy.registerUser('admin', 'pass', true);
+
+			// ユーザー作成
+			cy.registerUser('alice', 'alice1234');
+
+			cy.login('alice', 'alice1234');
+
+			// アカウント初期設定ウィザード
+			// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
+			cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click();
+			cy.wait(500);
+			cy.get('[data-cy-modal-dialog-ok]').click();
+		});
+
+		it('redirect to user profile', () => {
+			// テストのためだけに用意されたリダイレクト用ルートに飛ぶ
+			cy.visit('/redirect-test');
+
+			// プロフィールページのURLであることを確認する
+			cy.url().should('include', '/@alice')
+		});
+	});
+});
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 28058c338b..ccd9df83ed 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -93,6 +93,13 @@ windowRouter.addListener('push', ctx => {
 	history.value.push({ path: ctx.path, key: ctx.key });
 });
 
+windowRouter.addListener('replace', ctx => {
+	history.value.pop();
+	history.value.push({ path: ctx.path, key: ctx.key });
+});
+
+windowRouter.init();
+
 provide('router', windowRouter);
 provideMetadataReceiver((info) => {
 	pageMetadata.value = info;
diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/global/router/definition.ts
index 0333770a64..241b4fbcc7 100644
--- a/packages/frontend/src/global/router/definition.ts
+++ b/packages/frontend/src/global/router/definition.ts
@@ -4,6 +4,7 @@
  */
 
 import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
+import type { RouteDef } from '@/nirax.js';
 import { IRouter, Router } from '@/nirax.js';
 import { $i, iAmModerator } from '@/account.js';
 import MkLoading from '@/pages/_loading_.vue';
@@ -16,7 +17,7 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
 	errorComponent: MkError,
 });
 
-const routes = [{
+const routes: RouteDef[] = [{
 	path: '/@:initUser/pages/:initPageName/view-source',
 	component: page(() => import('@/pages/page-editor/page-editor.vue')),
 }, {
@@ -333,8 +334,7 @@ const routes = [{
 	component: page(() => import('@/pages/registry.vue')),
 }, {
 	path: '/install-extentions',
-	// Note: This path is kept for compatibility. It may be deleted.
-	component: page(() => import('@/pages/install-extensions.vue')),
+	redirect: '/install-extensions',
 	loginRequired: true,
 }, {
 	path: '/install-extensions',
@@ -557,6 +557,11 @@ const routes = [{
 	path: '/',
 	component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
 	globalCacheKey: 'index',
+}, {
+	// テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
+	path: '/redirect-test',
+	redirect: $i ? `@${$i.username}` : '/',
+	loginRequired: true,
 }, {
 	path: '/:(*)',
 	component: page(() => import('@/pages/not-found.vue')),
@@ -575,8 +580,6 @@ export function setupRouter(app: App) {
 
 	const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
 
-	window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
-
 	window.addEventListener('popstate', (event) => {
 		mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
 	});
@@ -585,5 +588,11 @@ export function setupRouter(app: App) {
 		window.history.pushState({ key: ctx.key }, '', ctx.path);
 	});
 
+	mainRouter.addListener('replace', ctx => {
+		window.history.replaceState({ key: ctx.key }, '', ctx.path);
+	});
+
+	mainRouter.init();
+
 	setMainRouter(mainRouter);
 }
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index a56aa6419e..ddb2a085db 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -9,16 +9,25 @@ import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
 
-export type RouteDef = {
+interface RouteDefBase {
 	path: string;
-	component: Component;
 	query?: Record<string, string>;
 	loginRequired?: boolean;
 	name?: string;
 	hash?: string;
 	globalCacheKey?: string;
 	children?: RouteDef[];
-};
+}
+
+interface RouteDefWithComponent extends RouteDefBase {
+	component: Component,
+}
+
+interface RouteDefWithRedirect extends RouteDefBase {
+	redirect: string | ((props: Map<string, string | boolean>) => string);
+}
+
+export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
 
 type ParsedPath = (string | {
 	name: string;
@@ -48,7 +57,19 @@ export type RouterEvent = {
 	same: () => void;
 }
 
-export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
+export type Resolved = {
+	route: RouteDef;
+	props: Map<string, string | boolean>;
+	child?: Resolved;
+	redirected?: boolean;
+
+	/** @internal */
+	_parsedRoute: {
+		fullPath: string;
+		queryString: string | null;
+		hash: string | null;
+	};
+};
 
 function parsePath(path: string): ParsedPath {
 	const res = [] as ParsedPath;
@@ -81,6 +102,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 	currentRoute: ShallowRef<RouteDef>;
 	navHook: ((path: string, flag?: any) => boolean) | null;
 
+	/**
+	 * ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
+	 */
+	init(): void;
+
 	resolve(path: string): Resolved | null;
 
 	getCurrentPath(): any;
@@ -156,12 +182,13 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	private routes: RouteDef[];
 	public current: Resolved;
-	public currentRef: ShallowRef<Resolved> = shallowRef();
-	public currentRoute: ShallowRef<RouteDef> = shallowRef();
+	public currentRef: ShallowRef<Resolved>;
+	public currentRoute: ShallowRef<RouteDef>;
 	private currentPath: string;
 	private isLoggedIn: boolean;
 	private notFoundPageComponent: Component;
 	private currentKey = Date.now().toString();
+	private redirectCount = 0;
 
 	public navHook: ((path: string, flag?: any) => boolean) | null = null;
 
@@ -169,13 +196,24 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		super();
 
 		this.routes = routes;
+		this.current = this.resolve(currentPath)!;
+		this.currentRef = shallowRef(this.current);
+		this.currentRoute = shallowRef(this.current.route);
 		this.currentPath = currentPath;
 		this.isLoggedIn = isLoggedIn;
 		this.notFoundPageComponent = notFoundPageComponent;
-		this.navigate(currentPath, null, false);
+	}
+
+	public init() {
+		const res = this.navigate(this.currentPath, null, false);
+		this.emit('replace', {
+			path: res._parsedRoute.fullPath,
+			key: this.currentKey,
+		});
 	}
 
 	public resolve(path: string): Resolved | null {
+		const fullPath = path;
 		let queryString: string | null = null;
 		let hash: string | null = null;
 		if (path[0] === '/') path = path.substring(1);
@@ -188,6 +226,12 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 			path = path.substring(0, path.indexOf('?'));
 		}
 
+		const _parsedRoute = {
+			fullPath,
+			queryString,
+			hash,
+		};
+
 		if (_DEV_) console.log('Routing: ', path, queryString);
 
 		function check(routes: RouteDef[], _parts: string[]): Resolved | null {
@@ -238,6 +282,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 								route,
 								props,
 								child,
+								_parsedRoute,
 							};
 						} else {
 							continue forEachRouteLoop;
@@ -263,6 +308,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 					return {
 						route,
 						props,
+						_parsedRoute,
 					};
 				} else {
 					if (route.children) {
@@ -272,6 +318,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 								route,
 								props,
 								child,
+								_parsedRoute,
 							};
 						} else {
 							continue forEachRouteLoop;
@@ -290,7 +337,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		return check(this.routes, _parts);
 	}
 
-	private navigate(path: string, key: string | null | undefined, emitChange = true) {
+	private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
 		const beforePath = this.currentPath;
 		this.currentPath = path;
 
@@ -300,6 +347,20 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 			throw new Error('no route found for: ' + path);
 		}
 
+		if ('redirect' in res.route) {
+			let redirectPath: string;
+			if (typeof res.route.redirect === 'function') {
+				redirectPath = res.route.redirect(res.props);
+			} else {
+				redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : '');
+			}
+			if (_DEV_) console.log('Redirecting to: ', redirectPath);
+			if (_redirected && this.redirectCount++ > 10) {
+				throw new Error('redirect loop detected');
+			}
+			return this.navigate(redirectPath, null, emitChange, true);
+		}
+
 		if (res.route.loginRequired && !this.isLoggedIn) {
 			res.route.component = this.notFoundPageComponent;
 			res.props.set('showLoginPopup', true);
@@ -321,7 +382,11 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 			});
 		}
 
-		return res;
+		this.redirectCount = 0;
+		return {
+			...res,
+			redirected: _redirected,
+		};
 	}
 
 	public getCurrentPath() {
@@ -345,7 +410,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		const res = this.navigate(path, null);
 		this.emit('push', {
 			beforePath,
-			path,
+			path: res._parsedRoute.fullPath,
 			route: res.route,
 			props: res.props,
 			key: this.currentKey,
@@ -353,7 +418,11 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	}
 
 	public replace(path: string, key?: string | null) {
-		this.navigate(path, key);
+		const res = this.navigate(path, key);
+		this.emit('replace', {
+			path: res._parsedRoute.fullPath,
+			key: this.currentKey,
+		});
 	}
 }