From 04f9147db6c0b3aff3347a62659f3dfb21fc3f94 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
 <46447427+samunohito@users.noreply.github.com>
Date: Mon, 8 Jan 2024 14:44:43 +0900
Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20router.ts=E8=A7=A3?=
 =?UTF-8?q?=E3=81=8D=E3=81=BB=E3=81=90=E3=81=97=20=20(#12907)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor(frontend): router.ts解きほぐし

* add debug hmr option

* fix comment

* fix not working

* add comment

* fix name

* Update definition.ts

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 packages/frontend/package.json                |   2 +-
 packages/frontend/src/boot/common.ts          |   3 +
 packages/frontend/src/boot/main-boot.ts       |  10 +-
 .../frontend/src/components/MkDrive.file.vue  |   2 +-
 .../frontend/src/components/MkPageWindow.vue  |  33 +-
 .../frontend/src/components/global/MkA.vue    |   2 +-
 .../src/components/global/RouterView.vue      |   6 +-
 .../frontend/src/global/router/definition.ts  | 571 ++++++++++++++++++
 packages/frontend/src/global/router/main.ts   | 163 +++++
 .../frontend/src/global/router/supplier.ts    |  30 +
 packages/frontend/src/nirax.ts                | 126 +++-
 packages/frontend/src/pages/admin/index.vue   |   2 +-
 .../frontend/src/pages/admin/roles.edit.vue   |   2 +-
 .../frontend/src/pages/admin/roles.role.vue   |   2 +-
 packages/frontend/src/pages/admin/roles.vue   |   2 +-
 .../frontend/src/pages/antenna-timeline.vue   |   2 +-
 .../frontend/src/pages/channel-editor.vue     |   2 +-
 packages/frontend/src/pages/channel.vue       |   2 +-
 packages/frontend/src/pages/channels.vue      |   2 +-
 .../frontend/src/pages/drive.file.info.vue    |   2 +-
 .../frontend/src/pages/flash/flash-edit.vue   |   2 +-
 .../frontend/src/pages/flash/flash-index.vue  |   2 +-
 packages/frontend/src/pages/follow.vue        |   2 +-
 packages/frontend/src/pages/gallery/edit.vue  |   2 +-
 packages/frontend/src/pages/gallery/index.vue |   2 +-
 packages/frontend/src/pages/gallery/post.vue  |   2 +-
 .../frontend/src/pages/my-antennas/create.vue |   2 +-
 .../frontend/src/pages/my-antennas/edit.vue   |   2 +-
 packages/frontend/src/pages/my-lists/list.vue |   2 +-
 .../src/pages/page-editor/page-editor.vue     |   2 +-
 packages/frontend/src/pages/pages.vue         |   2 +-
 .../frontend/src/pages/reset-password.vue     |   2 +-
 packages/frontend/src/pages/search.note.vue   |   2 +-
 packages/frontend/src/pages/search.user.vue   |   2 +-
 .../frontend/src/pages/settings/index.vue     |   2 +-
 .../src/pages/settings/webhook.edit.vue       |   2 +-
 .../frontend/src/pages/user-list-timeline.vue |   2 +-
 packages/frontend/src/pages/user/home.vue     |   2 +-
 packages/frontend/src/router.ts               | 561 -----------------
 .../frontend/src/scripts/get-user-menu.ts     |   6 +-
 packages/frontend/src/scripts/lookup.ts       |   2 +-
 .../frontend/src/ui/_common_/sw-inject.ts     |   2 +-
 packages/frontend/src/ui/classic.vue          |   2 +-
 packages/frontend/src/ui/deck.vue             |   2 +-
 packages/frontend/src/ui/deck/main-column.vue |   2 +-
 packages/frontend/src/ui/minimum.vue          |   2 +-
 packages/frontend/src/ui/universal.vue        |   2 +-
 packages/frontend/src/ui/visitor.vue          |   2 +-
 packages/frontend/src/ui/zen.vue              |   2 +-
 49 files changed, 937 insertions(+), 650 deletions(-)
 create mode 100644 packages/frontend/src/global/router/definition.ts
 create mode 100644 packages/frontend/src/global/router/main.ts
 create mode 100644 packages/frontend/src/global/router/supplier.ts
 delete mode 100644 packages/frontend/src/router.ts

diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 7e7559d825..9ef18a56a7 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -4,7 +4,7 @@
 	"type": "module",
 	"scripts": {
 		"watch": "vite",
-		"dev": "vite --config vite.config.local-dev.ts",
+		"dev": "vite --config vite.config.local-dev.ts --debug hmr",
 		"build": "vite build",
 		"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
 		"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index ef69eff764..c67911c9c3 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { fetchCustomEmojis } from '@/custom-emojis.js';
+import { setupRouter } from '@/global/router/definition.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Misskey v${version}`);
@@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
 
 	const app = createVue();
 
+	setupRouter(app);
+
 	if (_DEV_) {
 		app.config.performance = true;
 	}
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 0159d0c032..5011ce9e74 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -3,23 +3,23 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { createApp, markRaw, defineAsyncComponent } from 'vue';
+import { createApp, defineAsyncComponent, markRaw } from 'vue';
 import { common } from './common.js';
 import { ui } from '@/config.js';
 import { i18n } from '@/i18n.js';
-import { confirm, alert, post, popup, toast } from '@/os.js';
+import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
 import * as sound from '@/scripts/sound.js';
-import { $i, updateAccount, signout } from '@/account.js';
-import { defaultStore, ColdDeviceStorage } from '@/store.js';
+import { $i, signout, updateAccount } from '@/account.js';
+import { ColdDeviceStorage, defaultStore } from '@/store.js';
 import { makeHotkey } from '@/scripts/hotkey.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
-import { mainRouter } from '@/router.js';
 import { initializeSw } from '@/scripts/initialize-sw.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { mainRouter } from '@/global/router/main.js';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => createApp(
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index b46b25eba2..8a74319f29 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -45,9 +45,9 @@ import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { useRouter } from '@/router.js';
 import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
 import { deviceKind } from '@/scripts/device-kind.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 2647ace7db..28058c338b 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<div ref="contents" :class="$style.root" style="container-type: inline-size;">
-		<RouterView :key="reloadCount" :router="router"/>
+		<RouterView :key="reloadCount" :router="windowRouter"/>
 	</div>
 </MkWindow>
 </template>
 
 <script lang="ts" setup>
-import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
+import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
 import RouterView from '@/components/global/RouterView.vue';
 import MkWindow from '@/components/MkWindow.vue';
 import { popout as _popout } from '@/scripts/popout.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
-import { mainRouter, routes, page } from '@/router.js';
-import { $i } from '@/account.js';
-import { Router, useScrollPositionManager } from '@/nirax.js';
+import { useScrollPositionManager } from '@/nirax.js';
 import { i18n } from '@/i18n.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { openingWindowsCount } from '@/os.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { getScrollContainer } from '@/scripts/scroll.js';
+import { useRouterFactory } from '@/global/router/supplier.js';
+import { mainRouter } from '@/global/router/main.js';
 
 const props = defineProps<{
 	initialPath: string;
@@ -52,14 +52,15 @@ defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
+const routerFactory = useRouterFactory();
+const windowRouter = routerFactory(props.initialPath);
 
 const contents = shallowRef<HTMLElement>();
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
 const history = ref<{ path: string; key: any; }[]>([{
-	path: router.getCurrentPath(),
-	key: router.getCurrentKey(),
+	path: windowRouter.getCurrentPath(),
+	key: windowRouter.getCurrentKey(),
 }]);
 const buttonsLeft = computed(() => {
 	const buttons = [];
@@ -88,11 +89,11 @@ const buttonsRight = computed(() => {
 });
 const reloadCount = ref(0);
 
-router.addListener('push', ctx => {
+windowRouter.addListener('push', ctx => {
 	history.value.push({ path: ctx.path, key: ctx.key });
 });
 
-provide('router', router);
+provide('router', windowRouter);
 provideMetadataReceiver((info) => {
 	pageMetadata.value = info;
 });
@@ -112,20 +113,20 @@ const contextmenu = computed(() => ([{
 	icon: 'ti ti-external-link',
 	text: i18n.ts.openInNewTab,
 	action: () => {
-		window.open(url + router.getCurrentPath(), '_blank', 'noopener');
+		window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
 		windowEl.value.close();
 	},
 }, {
 	icon: 'ti ti-link',
 	text: i18n.ts.copyLink,
 	action: () => {
-		copyToClipboard(url + router.getCurrentPath());
+		copyToClipboard(url + windowRouter.getCurrentPath());
 	},
 }]));
 
 function back() {
 	history.value.pop();
-	router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
+	windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
 }
 
 function reload() {
@@ -137,16 +138,16 @@ function close() {
 }
 
 function expand() {
-	mainRouter.push(router.getCurrentPath(), 'forcePage');
+	mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
 	windowEl.value.close();
 }
 
 function popout() {
-	_popout(router.getCurrentPath(), windowEl.value.$el);
+	_popout(windowRouter.getCurrentPath(), windowEl.value.$el);
 	windowEl.value.close();
 }
 
-useScrollPositionManager(() => getScrollContainer(contents.value), router);
+useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
 
 onMounted(() => {
 	openingWindowsCount.value++;
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index d34f47a68a..fbea279dbe 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -15,7 +15,7 @@ import * as os from '@/os.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const props = withDefaults(defineProps<{
 	to: string;
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 99ed8adbef..dc7474835d 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
-import { Resolved, Router } from '@/nirax.js';
+import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
+import { IRouter, Resolved } from '@/nirax.js';
 import { defaultStore } from '@/store.js';
 
 const props = defineProps<{
-	router?: Router;
+	router?: IRouter;
 }>();
 
 const router = props.router ?? inject('router');
diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/global/router/definition.ts
new file mode 100644
index 0000000000..727d6b1bb2
--- /dev/null
+++ b/packages/frontend/src/global/router/definition.ts
@@ -0,0 +1,571 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
+import { IRouter, Router } from '@/nirax.js';
+import { $i, iAmModerator } from '@/account.js';
+import MkLoading from '@/pages/_loading_.vue';
+import MkError from '@/pages/_error_.vue';
+import { setMainRouter } from '@/global/router/main.js';
+
+const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
+	loader: loader,
+	loadingComponent: MkLoading,
+	errorComponent: MkError,
+});
+const routes = [{
+	path: '/@:initUser/pages/:initPageName/view-source',
+	component: page(() => import('@/pages/page-editor/page-editor.vue')),
+}, {
+	path: '/@:username/pages/:pageName',
+	component: page(() => import('@/pages/page.vue')),
+}, {
+	path: '/@:acct/following',
+	component: page(() => import('@/pages/user/following.vue')),
+}, {
+	path: '/@:acct/followers',
+	component: page(() => import('@/pages/user/followers.vue')),
+}, {
+	name: 'user',
+	path: '/@:acct/:page?',
+	component: page(() => import('@/pages/user/index.vue')),
+}, {
+	name: 'note',
+	path: '/notes/:noteId',
+	component: page(() => import('@/pages/note.vue')),
+}, {
+	name: 'list',
+	path: '/list/:listId',
+	component: page(() => import('@/pages/list.vue')),
+}, {
+	path: '/clips/:clipId',
+	component: page(() => import('@/pages/clip.vue')),
+}, {
+	path: '/instance-info/:host',
+	component: page(() => import('@/pages/instance-info.vue')),
+}, {
+	name: 'settings',
+	path: '/settings',
+	component: page(() => import('@/pages/settings/index.vue')),
+	loginRequired: true,
+	children: [{
+		path: '/profile',
+		name: 'profile',
+		component: page(() => import('@/pages/settings/profile.vue')),
+	}, {
+		path: '/avatar-decoration',
+		name: 'avatarDecoration',
+		component: page(() => import('@/pages/settings/avatar-decoration.vue')),
+	}, {
+		path: '/roles',
+		name: 'roles',
+		component: page(() => import('@/pages/settings/roles.vue')),
+	}, {
+		path: '/privacy',
+		name: 'privacy',
+		component: page(() => import('@/pages/settings/privacy.vue')),
+	}, {
+		path: '/emoji-picker',
+		name: 'emojiPicker',
+		component: page(() => import('@/pages/settings/emoji-picker.vue')),
+	}, {
+		path: '/drive',
+		name: 'drive',
+		component: page(() => import('@/pages/settings/drive.vue')),
+	}, {
+		path: '/drive/cleaner',
+		name: 'drive',
+		component: page(() => import('@/pages/settings/drive-cleaner.vue')),
+	}, {
+		path: '/notifications',
+		name: 'notifications',
+		component: page(() => import('@/pages/settings/notifications.vue')),
+	}, {
+		path: '/email',
+		name: 'email',
+		component: page(() => import('@/pages/settings/email.vue')),
+	}, {
+		path: '/security',
+		name: 'security',
+		component: page(() => import('@/pages/settings/security.vue')),
+	}, {
+		path: '/general',
+		name: 'general',
+		component: page(() => import('@/pages/settings/general.vue')),
+	}, {
+		path: '/theme/install',
+		name: 'theme',
+		component: page(() => import('@/pages/settings/theme.install.vue')),
+	}, {
+		path: '/theme/manage',
+		name: 'theme',
+		component: page(() => import('@/pages/settings/theme.manage.vue')),
+	}, {
+		path: '/theme',
+		name: 'theme',
+		component: page(() => import('@/pages/settings/theme.vue')),
+	}, {
+		path: '/navbar',
+		name: 'navbar',
+		component: page(() => import('@/pages/settings/navbar.vue')),
+	}, {
+		path: '/statusbar',
+		name: 'statusbar',
+		component: page(() => import('@/pages/settings/statusbar.vue')),
+	}, {
+		path: '/sounds',
+		name: 'sounds',
+		component: page(() => import('@/pages/settings/sounds.vue')),
+	}, {
+		path: '/plugin/install',
+		name: 'plugin',
+		component: page(() => import('@/pages/settings/plugin.install.vue')),
+	}, {
+		path: '/plugin',
+		name: 'plugin',
+		component: page(() => import('@/pages/settings/plugin.vue')),
+	}, {
+		path: '/import-export',
+		name: 'import-export',
+		component: page(() => import('@/pages/settings/import-export.vue')),
+	}, {
+		path: '/mute-block',
+		name: 'mute-block',
+		component: page(() => import('@/pages/settings/mute-block.vue')),
+	}, {
+		path: '/api',
+		name: 'api',
+		component: page(() => import('@/pages/settings/api.vue')),
+	}, {
+		path: '/apps',
+		name: 'api',
+		component: page(() => import('@/pages/settings/apps.vue')),
+	}, {
+		path: '/webhook/edit/:webhookId',
+		name: 'webhook',
+		component: page(() => import('@/pages/settings/webhook.edit.vue')),
+	}, {
+		path: '/webhook/new',
+		name: 'webhook',
+		component: page(() => import('@/pages/settings/webhook.new.vue')),
+	}, {
+		path: '/webhook',
+		name: 'webhook',
+		component: page(() => import('@/pages/settings/webhook.vue')),
+	}, {
+		path: '/deck',
+		name: 'deck',
+		component: page(() => import('@/pages/settings/deck.vue')),
+	}, {
+		path: '/preferences-backups',
+		name: 'preferences-backups',
+		component: page(() => import('@/pages/settings/preferences-backups.vue')),
+	}, {
+		path: '/migration',
+		name: 'migration',
+		component: page(() => import('@/pages/settings/migration.vue')),
+	}, {
+		path: '/custom-css',
+		name: 'general',
+		component: page(() => import('@/pages/settings/custom-css.vue')),
+	}, {
+		path: '/accounts',
+		name: 'profile',
+		component: page(() => import('@/pages/settings/accounts.vue')),
+	}, {
+		path: '/other',
+		name: 'other',
+		component: page(() => import('@/pages/settings/other.vue')),
+	}, {
+		path: '/',
+		component: page(() => import('@/pages/_empty_.vue')),
+	}],
+}, {
+	path: '/reset-password/:token?',
+	component: page(() => import('@/pages/reset-password.vue')),
+}, {
+	path: '/signup-complete/:code',
+	component: page(() => import('@/pages/signup-complete.vue')),
+}, {
+	path: '/announcements',
+	component: page(() => import('@/pages/announcements.vue')),
+}, {
+	path: '/about',
+	component: page(() => import('@/pages/about.vue')),
+	hash: 'initialTab',
+}, {
+	path: '/about-misskey',
+	component: page(() => import('@/pages/about-misskey.vue')),
+}, {
+	path: '/invite',
+	name: 'invite',
+	component: page(() => import('@/pages/invite.vue')),
+}, {
+	path: '/ads',
+	component: page(() => import('@/pages/ads.vue')),
+}, {
+	path: '/theme-editor',
+	component: page(() => import('@/pages/theme-editor.vue')),
+	loginRequired: true,
+}, {
+	path: '/roles/:role',
+	component: page(() => import('@/pages/role.vue')),
+}, {
+	path: '/user-tags/:tag',
+	component: page(() => import('@/pages/user-tag.vue')),
+}, {
+	path: '/explore',
+	component: page(() => import('@/pages/explore.vue')),
+	hash: 'initialTab',
+}, {
+	path: '/search',
+	component: page(() => import('@/pages/search.vue')),
+	query: {
+		q: 'query',
+		channel: 'channel',
+		type: 'type',
+		origin: 'origin',
+	},
+}, {
+	path: '/authorize-follow',
+	component: page(() => import('@/pages/follow.vue')),
+	loginRequired: true,
+}, {
+	path: '/share',
+	component: page(() => import('@/pages/share.vue')),
+	loginRequired: true,
+}, {
+	path: '/api-console',
+	component: page(() => import('@/pages/api-console.vue')),
+	loginRequired: true,
+}, {
+	path: '/scratchpad',
+	component: page(() => import('@/pages/scratchpad.vue')),
+}, {
+	path: '/auth/:token',
+	component: page(() => import('@/pages/auth.vue')),
+}, {
+	path: '/miauth/:session',
+	component: page(() => import('@/pages/miauth.vue')),
+	query: {
+		callback: 'callback',
+		name: 'name',
+		icon: 'icon',
+		permission: 'permission',
+	},
+}, {
+	path: '/oauth/authorize',
+	component: page(() => import('@/pages/oauth.vue')),
+}, {
+	path: '/tags/:tag',
+	component: page(() => import('@/pages/tag.vue')),
+}, {
+	path: '/pages/new',
+	component: page(() => import('@/pages/page-editor/page-editor.vue')),
+	loginRequired: true,
+}, {
+	path: '/pages/edit/:initPageId',
+	component: page(() => import('@/pages/page-editor/page-editor.vue')),
+	loginRequired: true,
+}, {
+	path: '/pages',
+	component: page(() => import('@/pages/pages.vue')),
+}, {
+	path: '/play/:id/edit',
+	component: page(() => import('@/pages/flash/flash-edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/play/new',
+	component: page(() => import('@/pages/flash/flash-edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/play/:id',
+	component: page(() => import('@/pages/flash/flash.vue')),
+}, {
+	path: '/play',
+	component: page(() => import('@/pages/flash/flash-index.vue')),
+}, {
+	path: '/gallery/:postId/edit',
+	component: page(() => import('@/pages/gallery/edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/gallery/new',
+	component: page(() => import('@/pages/gallery/edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/gallery/:postId',
+	component: page(() => import('@/pages/gallery/post.vue')),
+}, {
+	path: '/gallery',
+	component: page(() => import('@/pages/gallery/index.vue')),
+}, {
+	path: '/channels/:channelId/edit',
+	component: page(() => import('@/pages/channel-editor.vue')),
+	loginRequired: true,
+}, {
+	path: '/channels/new',
+	component: page(() => import('@/pages/channel-editor.vue')),
+	loginRequired: true,
+}, {
+	path: '/channels/:channelId',
+	component: page(() => import('@/pages/channel.vue')),
+}, {
+	path: '/channels',
+	component: page(() => import('@/pages/channels.vue')),
+}, {
+	path: '/custom-emojis-manager',
+	component: page(() => import('@/pages/custom-emojis-manager.vue')),
+}, {
+	path: '/avatar-decorations',
+	name: 'avatarDecorations',
+	component: page(() => import('@/pages/avatar-decorations.vue')),
+}, {
+	path: '/registry/keys/:domain/:path(*)?',
+	component: page(() => import('@/pages/registry.keys.vue')),
+}, {
+	path: '/registry/value/:domain/:path(*)?',
+	component: page(() => import('@/pages/registry.value.vue')),
+}, {
+	path: '/registry',
+	component: page(() => import('@/pages/registry.vue')),
+}, {
+	path: '/install-extentions',
+	component: page(() => import('@/pages/install-extentions.vue')),
+	loginRequired: true,
+}, {
+	path: '/admin/user/:userId',
+	component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')),
+}, {
+	path: '/admin/file/:fileId',
+	component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')),
+}, {
+	path: '/admin',
+	component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')),
+	children: [{
+		path: '/overview',
+		name: 'overview',
+		component: page(() => import('@/pages/admin/overview.vue')),
+	}, {
+		path: '/users',
+		name: 'users',
+		component: page(() => import('@/pages/admin/users.vue')),
+	}, {
+		path: '/emojis',
+		name: 'emojis',
+		component: page(() => import('@/pages/custom-emojis-manager.vue')),
+	}, {
+		path: '/avatar-decorations',
+		name: 'avatarDecorations',
+		component: page(() => import('@/pages/avatar-decorations.vue')),
+	}, {
+		path: '/queue',
+		name: 'queue',
+		component: page(() => import('@/pages/admin/queue.vue')),
+	}, {
+		path: '/files',
+		name: 'files',
+		component: page(() => import('@/pages/admin/files.vue')),
+	}, {
+		path: '/federation',
+		name: 'federation',
+		component: page(() => import('@/pages/admin/federation.vue')),
+	}, {
+		path: '/announcements',
+		name: 'announcements',
+		component: page(() => import('@/pages/admin/announcements.vue')),
+	}, {
+		path: '/ads',
+		name: 'ads',
+		component: page(() => import('@/pages/admin/ads.vue')),
+	}, {
+		path: '/roles/:id/edit',
+		name: 'roles',
+		component: page(() => import('@/pages/admin/roles.edit.vue')),
+	}, {
+		path: '/roles/new',
+		name: 'roles',
+		component: page(() => import('@/pages/admin/roles.edit.vue')),
+	}, {
+		path: '/roles/:id',
+		name: 'roles',
+		component: page(() => import('@/pages/admin/roles.role.vue')),
+	}, {
+		path: '/roles',
+		name: 'roles',
+		component: page(() => import('@/pages/admin/roles.vue')),
+	}, {
+		path: '/database',
+		name: 'database',
+		component: page(() => import('@/pages/admin/database.vue')),
+	}, {
+		path: '/abuses',
+		name: 'abuses',
+		component: page(() => import('@/pages/admin/abuses.vue')),
+	}, {
+		path: '/modlog',
+		name: 'modlog',
+		component: page(() => import('@/pages/admin/modlog.vue')),
+	}, {
+		path: '/settings',
+		name: 'settings',
+		component: page(() => import('@/pages/admin/settings.vue')),
+	}, {
+		path: '/branding',
+		name: 'branding',
+		component: page(() => import('@/pages/admin/branding.vue')),
+	}, {
+		path: '/moderation',
+		name: 'moderation',
+		component: page(() => import('@/pages/admin/moderation.vue')),
+	}, {
+		path: '/email-settings',
+		name: 'email-settings',
+		component: page(() => import('@/pages/admin/email-settings.vue')),
+	}, {
+		path: '/object-storage',
+		name: 'object-storage',
+		component: page(() => import('@/pages/admin/object-storage.vue')),
+	}, {
+		path: '/security',
+		name: 'security',
+		component: page(() => import('@/pages/admin/security.vue')),
+	}, {
+		path: '/relays',
+		name: 'relays',
+		component: page(() => import('@/pages/admin/relays.vue')),
+	}, {
+		path: '/instance-block',
+		name: 'instance-block',
+		component: page(() => import('@/pages/admin/instance-block.vue')),
+	}, {
+		path: '/proxy-account',
+		name: 'proxy-account',
+		component: page(() => import('@/pages/admin/proxy-account.vue')),
+	}, {
+		path: '/external-services',
+		name: 'external-services',
+		component: page(() => import('@/pages/admin/external-services.vue')),
+	}, {
+		path: '/other-settings',
+		name: 'other-settings',
+		component: page(() => import('@/pages/admin/other-settings.vue')),
+	}, {
+		path: '/server-rules',
+		name: 'server-rules',
+		component: page(() => import('@/pages/admin/server-rules.vue')),
+	}, {
+		path: '/invites',
+		name: 'invites',
+		component: page(() => import('@/pages/admin/invites.vue')),
+	}, {
+		path: '/',
+		component: page(() => import('@/pages/_empty_.vue')),
+	}],
+}, {
+	path: '/my/notifications',
+	component: page(() => import('@/pages/notifications.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/favorites',
+	component: page(() => import('@/pages/favorites.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/achievements',
+	component: page(() => import('@/pages/achievements.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/drive/folder/:folder',
+	component: page(() => import('@/pages/drive.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/drive',
+	component: page(() => import('@/pages/drive.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/drive/file/:fileId',
+	component: page(() => import('@/pages/drive.file.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/follow-requests',
+	component: page(() => import('@/pages/follow-requests.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/lists/:listId',
+	component: page(() => import('@/pages/my-lists/list.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/lists',
+	component: page(() => import('@/pages/my-lists/index.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/clips',
+	component: page(() => import('@/pages/my-clips/index.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/antennas/create',
+	component: page(() => import('@/pages/my-antennas/create.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/antennas/:antennaId',
+	component: page(() => import('@/pages/my-antennas/edit.vue')),
+	loginRequired: true,
+}, {
+	path: '/my/antennas',
+	component: page(() => import('@/pages/my-antennas/index.vue')),
+	loginRequired: true,
+}, {
+	path: '/timeline/list/:listId',
+	component: page(() => import('@/pages/user-list-timeline.vue')),
+	loginRequired: true,
+}, {
+	path: '/timeline/antenna/:antennaId',
+	component: page(() => import('@/pages/antenna-timeline.vue')),
+	loginRequired: true,
+}, {
+	path: '/clicker',
+	component: page(() => import('@/pages/clicker.vue')),
+	loginRequired: true,
+}, {
+	path: '/bubble-game',
+	component: page(() => import('@/pages/drop-and-fusion.vue')),
+	loginRequired: true,
+}, {
+	path: '/timeline',
+	component: page(() => import('@/pages/timeline.vue')),
+}, {
+	name: 'index',
+	path: '/',
+	component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
+	globalCacheKey: 'index',
+}, {
+	path: '/:(*)',
+	component: page(() => import('@/pages/not-found.vue')),
+}];
+
+function createRouterImpl(path: string): IRouter {
+	return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
+}
+
+/**
+ * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
+ * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
+ */
+export function setupRouter(app: App) {
+	app.provide('routerFactory', createRouterImpl);
+
+	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);
+	});
+
+	mainRouter.addListener('push', ctx => {
+		window.history.pushState({ key: ctx.key }, '', ctx.path);
+	});
+
+	setMainRouter(mainRouter);
+}
diff --git a/packages/frontend/src/global/router/main.ts b/packages/frontend/src/global/router/main.ts
new file mode 100644
index 0000000000..5adb3f606f
--- /dev/null
+++ b/packages/frontend/src/global/router/main.ts
@@ -0,0 +1,163 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ShallowRef } from 'vue';
+import { EventEmitter } from 'eventemitter3';
+import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
+
+function getMainRouter(): IRouter {
+	const router = mainRouterHolder;
+	if (!router) {
+		throw new Error('mainRouter is not found.');
+	}
+
+	return router;
+}
+
+/**
+ * メインルータを設定する。一度設定すると、それ以降は変更できない。
+ * {@link setupRouter}から呼び出されることのみを想定している。
+ */
+export function setMainRouter(router: IRouter) {
+	if (mainRouterHolder) {
+		throw new Error('mainRouter is already exists.');
+	}
+
+	mainRouterHolder = router;
+}
+
+/**
+ * {@link mainRouter}用のプロキシ実装。
+ * {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。
+ * その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。
+ */
+class MainRouterProxy implements IRouter {
+	private supplier: () => IRouter;
+
+	constructor(supplier: () => IRouter) {
+		this.supplier = supplier;
+	}
+
+	get current(): Resolved {
+		return this.supplier().current;
+	}
+
+	get currentRef(): ShallowRef<Resolved> {
+		return this.supplier().currentRef;
+	}
+
+	get currentRoute(): ShallowRef<RouteDef> {
+		return this.supplier().currentRoute;
+	}
+
+	get navHook(): ((path: string, flag?: any) => boolean) | null {
+		return this.supplier().navHook;
+	}
+
+	set navHook(value) {
+		this.supplier().navHook = value;
+	}
+
+	getCurrentKey(): string {
+		return this.supplier().getCurrentKey();
+	}
+
+	getCurrentPath(): any {
+		return this.supplier().getCurrentPath();
+	}
+
+	push(path: string, flag?: any): void {
+		this.supplier().push(path, flag);
+	}
+
+	replace(path: string, key?: string | null): void {
+		this.supplier().replace(path, key);
+	}
+
+	resolve(path: string): Resolved | null {
+		return this.supplier().resolve(path);
+	}
+
+	eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
+		return this.supplier().eventNames();
+	}
+
+	listeners<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+	): Array<EventEmitter.EventListener<RouterEvent, T>> {
+		return this.supplier().listeners(event);
+	}
+
+	listenerCount(
+		event: EventEmitter.EventNames<RouterEvent>,
+	): number {
+		return this.supplier().listenerCount(event);
+	}
+
+	emit<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		...args: EventEmitter.EventArgs<RouterEvent, T>
+	): boolean {
+		return this.supplier().emit(event, ...args);
+	}
+
+	on<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+	): this {
+		this.supplier().on(event, fn, context);
+		return this;
+	}
+
+	addListener<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+	): this {
+		this.supplier().addListener(event, fn, context);
+		return this;
+	}
+
+	once<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+	): this {
+		this.supplier().once(event, fn, context);
+		return this;
+	}
+
+	removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn?: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+		once?: boolean,
+	): this {
+		this.supplier().removeListener(event, fn, context, once);
+		return this;
+	}
+
+	off<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn?: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+		once?: boolean,
+	): this {
+		this.supplier().off(event, fn, context, once);
+		return this;
+	}
+
+	removeAllListeners(
+		event?: EventEmitter.EventNames<RouterEvent>,
+	): this {
+		this.supplier().removeAllListeners(event);
+		return this;
+	}
+}
+
+let mainRouterHolder: IRouter | null = null;
+
+export const mainRouter: IRouter = new MainRouterProxy(getMainRouter);
diff --git a/packages/frontend/src/global/router/supplier.ts b/packages/frontend/src/global/router/supplier.ts
new file mode 100644
index 0000000000..1e321ef21f
--- /dev/null
+++ b/packages/frontend/src/global/router/supplier.ts
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { inject } from 'vue';
+import { IRouter, Router } from '@/nirax.js';
+import { mainRouter } from '@/global/router/main.js';
+
+/**
+ * メインの{@link Router}を取得する。
+ * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない)
+ */
+export function useRouter(): IRouter {
+	return inject<Router | null>('router', null) ?? mainRouter;
+}
+
+/**
+ * 任意の{@link Router}を取得するためのファクトリを取得する。
+ * あらかじめ{@link setupRouter}を実行しておく必要がある。
+ */
+export function useRouterFactory(): (path: string) => IRouter {
+	const factory = inject<(path: string) => IRouter>('routerFactory');
+	if (!factory) {
+		console.error('routerFactory is not defined.');
+		throw new Error('routerFactory is not defined.');
+	}
+
+	return factory;
+}
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 9755bdcb18..a56aa6419e 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -5,11 +5,11 @@
 
 // NIRAX --- A lightweight router
 
-import { EventEmitter } from 'eventemitter3';
 import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
+import { EventEmitter } from 'eventemitter3';
 import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
 
-type RouteDef = {
+export type RouteDef = {
 	path: string;
 	component: Component;
 	query?: Record<string, string>;
@@ -27,6 +27,27 @@ type ParsedPath = (string | {
 	optional?: boolean;
 })[];
 
+export type RouterEvent = {
+	change: (ctx: {
+		beforePath: string;
+		path: string;
+		resolved: Resolved;
+		key: string;
+	}) => void;
+	replace: (ctx: {
+		path: string;
+		key: string;
+	}) => void;
+	push: (ctx: {
+		beforePath: string;
+		path: string;
+		route: RouteDef | null;
+		props: Map<string, string> | null;
+		key: string;
+	}) => void;
+	same: () => void;
+}
+
 export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
 
 function parsePath(path: string): ParsedPath {
@@ -54,26 +75,85 @@ function parsePath(path: string): ParsedPath {
 	return res;
 }
 
-export class Router extends EventEmitter<{
-	change: (ctx: {
-		beforePath: string;
-		path: string;
-		resolved: Resolved;
-		key: string;
-	}) => void;
-	replace: (ctx: {
-		path: string;
-		key: string;
-	}) => void;
-	push: (ctx: {
-		beforePath: string;
-		path: string;
-		route: RouteDef | null;
-		props: Map<string, string> | null;
-		key: string;
-	}) => void;
-	same: () => void;
-}> {
+export interface IRouter extends EventEmitter<RouterEvent> {
+	current: Resolved;
+	currentRef: ShallowRef<Resolved>;
+	currentRoute: ShallowRef<RouteDef>;
+	navHook: ((path: string, flag?: any) => boolean) | null;
+
+	resolve(path: string): Resolved | null;
+
+	getCurrentPath(): any;
+
+	getCurrentKey(): string;
+
+	push(path: string, flag?: any): void;
+
+	replace(path: string, key?: string | null): void;
+
+	/** @see EventEmitter */
+	eventNames(): Array<EventEmitter.EventNames<RouterEvent>>;
+
+	/** @see EventEmitter */
+	listeners<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T
+	): Array<EventEmitter.EventListener<RouterEvent, T>>;
+
+	/** @see EventEmitter */
+	listenerCount(
+		event: EventEmitter.EventNames<RouterEvent>
+	): number;
+
+	/** @see EventEmitter */
+	emit<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		...args: EventEmitter.EventArgs<RouterEvent, T>
+	): boolean;
+
+	/** @see EventEmitter */
+	on<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any
+	): this;
+
+	/** @see EventEmitter */
+	addListener<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any
+	): this;
+
+	/** @see EventEmitter */
+	once<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any
+	): this;
+
+	/** @see EventEmitter */
+	removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn?: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+		once?: boolean | undefined
+	): this;
+
+	/** @see EventEmitter */
+	off<T extends EventEmitter.EventNames<RouterEvent>>(
+		event: T,
+		fn?: EventEmitter.EventListener<RouterEvent, T>,
+		context?: any,
+		once?: boolean | undefined
+	): this;
+
+	/** @see EventEmitter */
+	removeAllListeners(
+		event?: EventEmitter.EventNames<RouterEvent>
+	): this;
+}
+
+export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	private routes: RouteDef[];
 	public current: Resolved;
 	public currentRef: ShallowRef<Resolved> = shallowRef();
@@ -277,7 +357,7 @@ export class Router extends EventEmitter<{
 	}
 }
 
-export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
+export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: IRouter) {
 	const scrollPosStore = new Map<string, number>();
 
 	onMounted(() => {
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 333bac724b..7106ed7438 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -36,8 +36,8 @@ import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
-import { useRouter } from '@/router.js';
 import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const isEmpty = (x: string | null) => x == null || x === '';
 
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index db0acae24a..82e230d6a6 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -31,9 +31,9 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
 import MkButton from '@/components/MkButton.vue';
 import { rolesCache } from '@/cache.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index d5ce190ef2..ff29f4ec1f 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -70,12 +70,12 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
 import MkButton from '@/components/MkButton.vue';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { infoImageUrl } from '@/instance.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index f7c4048b23..732affd77d 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -237,9 +237,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { instance } from '@/instance.js';
-import { useRouter } from '@/router.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { ROLE_POLICIES } from '@/const.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 const baseRoleQ = ref('');
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index d96ca4208b..7f07ac4987 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -30,9 +30,9 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@/scripts/scroll.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 727778b6e6..99b93444db 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -77,12 +77,12 @@ import MkColorInput from '@/components/MkColorInput.vue';
 import { selectFile } from '@/scripts/select-file.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
+import { useRouter } from '@/global/router/supplier.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 667563bd16..e698098f35 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -75,7 +75,6 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
 import { $i, iAmModerator } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -92,6 +91,7 @@ import { PageHeaderItem } from '@/types/page-header.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index b7cc5cd36e..80a401eee7 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -58,9 +58,9 @@ import MkInput from '@/components/MkInput.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { useRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 4c635028f3..64c3ad70ba 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -80,7 +80,7 @@ import { infoImageUrl } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index ce077779c8..8298dc6d79 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -45,7 +45,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const PRESET_DEFAULT = `/// @ 0.16.0
 
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index e0b9f87d46..7852018894 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -42,9 +42,9 @@ import { computed, ref } from 'vue';
 import MkFlashPreview from '@/components/MkFlashPreview.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
-import { useRouter } from '@/router.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue
index 5a21604080..eefef828bd 100644
--- a/packages/frontend/src/pages/follow.vue
+++ b/packages/frontend/src/pages/follow.vue
@@ -13,9 +13,9 @@ import { } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { mainRouter } from '@/router.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
+import { mainRouter } from '@/global/router/main.js';
 
 async function follow(user): Promise<void> {
 	const { canceled } = await os.confirm({
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index e0c7654531..f7db01ce95 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -48,9 +48,9 @@ import FormSuspense from '@/components/form/suspense.vue';
 import { selectFiles } from '@/scripts/select-file.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index 8d9ac07805..0198ab9700 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -53,7 +53,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index f71fe0f260..dcd427d6b4 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -72,13 +72,13 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import { url } from '@/config.js';
-import { useRouter } from '@/router.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { defaultStore } from '@/store.js';
 import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index c5b1b54222..61b9424bdd 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -14,8 +14,8 @@ import { ref } from 'vue';
 import XAntenna from './editor.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
 import { antennasCache } from '@/cache.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 0648f5340f..b4ca7cc9f8 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -15,9 +15,9 @@ import * as Misskey from 'misskey-js';
 import XAntenna from './editor.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache } from '@/cache.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 5798070ad8..85775a2fdd 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -58,7 +58,6 @@ import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { mainRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import { userPage } from '@/filters/user.js';
@@ -70,6 +69,7 @@ import { userListsCache } from '@/cache.js';
 import { signinRequired } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import MkPagination from '@/components/MkPagination.vue';
+import { mainRouter } from '@/global/router/main.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index 496a8c3274..6db72dccba 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -73,10 +73,10 @@ import { url } from '@/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { selectFile } from '@/scripts/select-file.js';
-import { mainRouter } from '@/router.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { $i } from '@/account.js';
+import { mainRouter } from '@/global/router/main.js';
 
 const props = defineProps<{
 	initPageId?: string;
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index bc51b55c7f..22ab9ced09 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -40,9 +40,9 @@ import { computed, ref } from 'vue';
 import MkPagePreview from '@/components/MkPagePreview.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
-import { useRouter } from '@/router.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index c9d193b787..d8dec27513 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { mainRouter } from '@/global/router/main.js';
 
 const props = defineProps<{
 	token?: string;
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 1b12910a38..811218faf5 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -51,8 +51,8 @@ import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { useRouter } from '@/router.js';
 import MkFolder from '@/components/MkFolder.vue';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index 5e9048ee57..82cedc9833 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -34,7 +34,7 @@ import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index ee0188873e..be443033bc 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -35,9 +35,9 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue';
 import { signout, $i } from '@/account.js';
 import { clearCache } from '@/scripts/clear-cache.js';
 import { instance } from '@/instance.js';
-import { useRouter } from '@/router.js';
 import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import * as os from '@/os.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const indexInfo = {
 	title: i18n.ts.settings,
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 9eb344bd46..a122c4c819 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -51,7 +51,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 19c376c77b..10a21ef20d 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@/scripts/scroll.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/router.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 5258165d7c..ed9722b7ed 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -166,13 +166,13 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
 import { i18n } from '@/i18n.js';
 import { $i, iAmModerator } from '@/account.js';
 import { dateString } from '@/filters/date.js';
 import { confetti } from '@/scripts/confetti.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { useRouter } from '@/global/router/supplier.js';
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
deleted file mode 100644
index 35478a35a9..0000000000
--- a/packages/frontend/src/router.ts
+++ /dev/null
@@ -1,561 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue';
-import { Router } from '@/nirax.js';
-import { $i, iAmModerator } from '@/account.js';
-import MkLoading from '@/pages/_loading_.vue';
-import MkError from '@/pages/_error_.vue';
-
-export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
-	loader: loader,
-	loadingComponent: MkLoading,
-	errorComponent: MkError,
-});
-
-export const routes = [{
-	path: '/@:initUser/pages/:initPageName/view-source',
-	component: page(() => import('./pages/page-editor/page-editor.vue')),
-}, {
-	path: '/@:username/pages/:pageName',
-	component: page(() => import('./pages/page.vue')),
-}, {
-	path: '/@:acct/following',
-	component: page(() => import('./pages/user/following.vue')),
-}, {
-	path: '/@:acct/followers',
-	component: page(() => import('./pages/user/followers.vue')),
-}, {
-	name: 'user',
-	path: '/@:acct/:page?',
-	component: page(() => import('./pages/user/index.vue')),
-}, {
-	name: 'note',
-	path: '/notes/:noteId',
-	component: page(() => import('./pages/note.vue')),
-}, {
-	name: 'list',
-	path: '/list/:listId',
-	component: page(() => import('./pages/list.vue')),
-}, {
-	path: '/clips/:clipId',
-	component: page(() => import('./pages/clip.vue')),
-}, {
-	path: '/instance-info/:host',
-	component: page(() => import('./pages/instance-info.vue')),
-}, {
-	name: 'settings',
-	path: '/settings',
-	component: page(() => import('./pages/settings/index.vue')),
-	loginRequired: true,
-	children: [{
-		path: '/profile',
-		name: 'profile',
-		component: page(() => import('./pages/settings/profile.vue')),
-	}, {
-		path: '/avatar-decoration',
-		name: 'avatarDecoration',
-		component: page(() => import('./pages/settings/avatar-decoration.vue')),
-	}, {
-		path: '/roles',
-		name: 'roles',
-		component: page(() => import('./pages/settings/roles.vue')),
-	}, {
-		path: '/privacy',
-		name: 'privacy',
-		component: page(() => import('./pages/settings/privacy.vue')),
-	}, {
-		path: '/emoji-picker',
-		name: 'emojiPicker',
-		component: page(() => import('./pages/settings/emoji-picker.vue')),
-	}, {
-		path: '/drive',
-		name: 'drive',
-		component: page(() => import('./pages/settings/drive.vue')),
-	}, {
-		path: '/drive/cleaner',
-		name: 'drive',
-		component: page(() => import('./pages/settings/drive-cleaner.vue')),
-	}, {
-		path: '/notifications',
-		name: 'notifications',
-		component: page(() => import('./pages/settings/notifications.vue')),
-	}, {
-		path: '/email',
-		name: 'email',
-		component: page(() => import('./pages/settings/email.vue')),
-	}, {
-		path: '/security',
-		name: 'security',
-		component: page(() => import('./pages/settings/security.vue')),
-	}, {
-		path: '/general',
-		name: 'general',
-		component: page(() => import('./pages/settings/general.vue')),
-	}, {
-		path: '/theme/install',
-		name: 'theme',
-		component: page(() => import('./pages/settings/theme.install.vue')),
-	}, {
-		path: '/theme/manage',
-		name: 'theme',
-		component: page(() => import('./pages/settings/theme.manage.vue')),
-	}, {
-		path: '/theme',
-		name: 'theme',
-		component: page(() => import('./pages/settings/theme.vue')),
-	}, {
-		path: '/navbar',
-		name: 'navbar',
-		component: page(() => import('./pages/settings/navbar.vue')),
-	}, {
-		path: '/statusbar',
-		name: 'statusbar',
-		component: page(() => import('./pages/settings/statusbar.vue')),
-	}, {
-		path: '/sounds',
-		name: 'sounds',
-		component: page(() => import('./pages/settings/sounds.vue')),
-	}, {
-		path: '/plugin/install',
-		name: 'plugin',
-		component: page(() => import('./pages/settings/plugin.install.vue')),
-	}, {
-		path: '/plugin',
-		name: 'plugin',
-		component: page(() => import('./pages/settings/plugin.vue')),
-	}, {
-		path: '/import-export',
-		name: 'import-export',
-		component: page(() => import('./pages/settings/import-export.vue')),
-	}, {
-		path: '/mute-block',
-		name: 'mute-block',
-		component: page(() => import('./pages/settings/mute-block.vue')),
-	}, {
-		path: '/api',
-		name: 'api',
-		component: page(() => import('./pages/settings/api.vue')),
-	}, {
-		path: '/apps',
-		name: 'api',
-		component: page(() => import('./pages/settings/apps.vue')),
-	}, {
-		path: '/webhook/edit/:webhookId',
-		name: 'webhook',
-		component: page(() => import('./pages/settings/webhook.edit.vue')),
-	}, {
-		path: '/webhook/new',
-		name: 'webhook',
-		component: page(() => import('./pages/settings/webhook.new.vue')),
-	}, {
-		path: '/webhook',
-		name: 'webhook',
-		component: page(() => import('./pages/settings/webhook.vue')),
-	}, {
-		path: '/deck',
-		name: 'deck',
-		component: page(() => import('./pages/settings/deck.vue')),
-	}, {
-		path: '/preferences-backups',
-		name: 'preferences-backups',
-		component: page(() => import('./pages/settings/preferences-backups.vue')),
-	}, {
-		path: '/migration',
-		name: 'migration',
-		component: page(() => import('./pages/settings/migration.vue')),
-	}, {
-		path: '/custom-css',
-		name: 'general',
-		component: page(() => import('./pages/settings/custom-css.vue')),
-	}, {
-		path: '/accounts',
-		name: 'profile',
-		component: page(() => import('./pages/settings/accounts.vue')),
-	}, {
-		path: '/other',
-		name: 'other',
-		component: page(() => import('./pages/settings/other.vue')),
-	}, {
-		path: '/',
-		component: page(() => import('./pages/_empty_.vue')),
-	}],
-}, {
-	path: '/reset-password/:token?',
-	component: page(() => import('./pages/reset-password.vue')),
-}, {
-	path: '/signup-complete/:code',
-	component: page(() => import('./pages/signup-complete.vue')),
-}, {
-	path: '/announcements',
-	component: page(() => import('./pages/announcements.vue')),
-}, {
-	path: '/about',
-	component: page(() => import('./pages/about.vue')),
-	hash: 'initialTab',
-}, {
-	path: '/about-misskey',
-	component: page(() => import('./pages/about-misskey.vue')),
-}, {
-	path: '/invite',
-	name: 'invite',
-	component: page(() => import('./pages/invite.vue')),
-}, {
-	path: '/ads',
-	component: page(() => import('./pages/ads.vue')),
-}, {
-	path: '/theme-editor',
-	component: page(() => import('./pages/theme-editor.vue')),
-	loginRequired: true,
-}, {
-	path: '/roles/:role',
-	component: page(() => import('./pages/role.vue')),
-}, {
-	path: '/user-tags/:tag',
-	component: page(() => import('./pages/user-tag.vue')),
-}, {
-	path: '/explore',
-	component: page(() => import('./pages/explore.vue')),
-	hash: 'initialTab',
-}, {
-	path: '/search',
-	component: page(() => import('./pages/search.vue')),
-	query: {
-		q: 'query',
-		channel: 'channel',
-		type: 'type',
-		origin: 'origin',
-	},
-}, {
-	path: '/authorize-follow',
-	component: page(() => import('./pages/follow.vue')),
-	loginRequired: true,
-}, {
-	path: '/share',
-	component: page(() => import('./pages/share.vue')),
-	loginRequired: true,
-}, {
-	path: '/api-console',
-	component: page(() => import('./pages/api-console.vue')),
-	loginRequired: true,
-}, {
-	path: '/scratchpad',
-	component: page(() => import('./pages/scratchpad.vue')),
-}, {
-	path: '/auth/:token',
-	component: page(() => import('./pages/auth.vue')),
-}, {
-	path: '/miauth/:session',
-	component: page(() => import('./pages/miauth.vue')),
-	query: {
-		callback: 'callback',
-		name: 'name',
-		icon: 'icon',
-		permission: 'permission',
-	},
-}, {
-	path: '/oauth/authorize',
-	component: page(() => import('./pages/oauth.vue')),
-}, {
-	path: '/tags/:tag',
-	component: page(() => import('./pages/tag.vue')),
-}, {
-	path: '/pages/new',
-	component: page(() => import('./pages/page-editor/page-editor.vue')),
-	loginRequired: true,
-}, {
-	path: '/pages/edit/:initPageId',
-	component: page(() => import('./pages/page-editor/page-editor.vue')),
-	loginRequired: true,
-}, {
-	path: '/pages',
-	component: page(() => import('./pages/pages.vue')),
-}, {
-	path: '/play/:id/edit',
-	component: page(() => import('./pages/flash/flash-edit.vue')),
-	loginRequired: true,
-}, {
-	path: '/play/new',
-	component: page(() => import('./pages/flash/flash-edit.vue')),
-	loginRequired: true,
-}, {
-	path: '/play/:id',
-	component: page(() => import('./pages/flash/flash.vue')),
-}, {
-	path: '/play',
-	component: page(() => import('./pages/flash/flash-index.vue')),
-}, {
-	path: '/gallery/:postId/edit',
-	component: page(() => import('./pages/gallery/edit.vue')),
-	loginRequired: true,
-}, {
-	path: '/gallery/new',
-	component: page(() => import('./pages/gallery/edit.vue')),
-	loginRequired: true,
-}, {
-	path: '/gallery/:postId',
-	component: page(() => import('./pages/gallery/post.vue')),
-}, {
-	path: '/gallery',
-	component: page(() => import('./pages/gallery/index.vue')),
-}, {
-	path: '/channels/:channelId/edit',
-	component: page(() => import('./pages/channel-editor.vue')),
-	loginRequired: true,
-}, {
-	path: '/channels/new',
-	component: page(() => import('./pages/channel-editor.vue')),
-	loginRequired: true,
-}, {
-	path: '/channels/:channelId',
-	component: page(() => import('./pages/channel.vue')),
-}, {
-	path: '/channels',
-	component: page(() => import('./pages/channels.vue')),
-}, {
-	path: '/custom-emojis-manager',
-	component: page(() => import('./pages/custom-emojis-manager.vue')),
-}, {
-	path: '/avatar-decorations',
-	name: 'avatarDecorations',
-	component: page(() => import('./pages/avatar-decorations.vue')),
-}, {
-	path: '/registry/keys/:domain/:path(*)?',
-	component: page(() => import('./pages/registry.keys.vue')),
-}, {
-	path: '/registry/value/:domain/:path(*)?',
-	component: page(() => import('./pages/registry.value.vue')),
-}, {
-	path: '/registry',
-	component: page(() => import('./pages/registry.vue')),
-}, {
-	path: '/install-extentions',
-	component: page(() => import('./pages/install-extentions.vue')),
-	loginRequired: true,
-}, {
-	path: '/admin/user/:userId',
-	component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')),
-}, {
-	path: '/admin/file/:fileId',
-	component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')),
-}, {
-	path: '/admin',
-	component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')),
-	children: [{
-		path: '/overview',
-		name: 'overview',
-		component: page(() => import('./pages/admin/overview.vue')),
-	}, {
-		path: '/users',
-		name: 'users',
-		component: page(() => import('./pages/admin/users.vue')),
-	}, {
-		path: '/emojis',
-		name: 'emojis',
-		component: page(() => import('./pages/custom-emojis-manager.vue')),
-	}, {
-		path: '/avatar-decorations',
-		name: 'avatarDecorations',
-		component: page(() => import('./pages/avatar-decorations.vue')),
-	}, {
-		path: '/queue',
-		name: 'queue',
-		component: page(() => import('./pages/admin/queue.vue')),
-	}, {
-		path: '/files',
-		name: 'files',
-		component: page(() => import('./pages/admin/files.vue')),
-	}, {
-		path: '/federation',
-		name: 'federation',
-		component: page(() => import('./pages/admin/federation.vue')),
-	}, {
-		path: '/announcements',
-		name: 'announcements',
-		component: page(() => import('./pages/admin/announcements.vue')),
-	}, {
-		path: '/ads',
-		name: 'ads',
-		component: page(() => import('./pages/admin/ads.vue')),
-	}, {
-		path: '/roles/:id/edit',
-		name: 'roles',
-		component: page(() => import('./pages/admin/roles.edit.vue')),
-	}, {
-		path: '/roles/new',
-		name: 'roles',
-		component: page(() => import('./pages/admin/roles.edit.vue')),
-	}, {
-		path: '/roles/:id',
-		name: 'roles',
-		component: page(() => import('./pages/admin/roles.role.vue')),
-	}, {
-		path: '/roles',
-		name: 'roles',
-		component: page(() => import('./pages/admin/roles.vue')),
-	}, {
-		path: '/database',
-		name: 'database',
-		component: page(() => import('./pages/admin/database.vue')),
-	}, {
-		path: '/abuses',
-		name: 'abuses',
-		component: page(() => import('./pages/admin/abuses.vue')),
-	}, {
-		path: '/modlog',
-		name: 'modlog',
-		component: page(() => import('./pages/admin/modlog.vue')),
-	}, {
-		path: '/settings',
-		name: 'settings',
-		component: page(() => import('./pages/admin/settings.vue')),
-	}, {
-		path: '/branding',
-		name: 'branding',
-		component: page(() => import('./pages/admin/branding.vue')),
-	}, {
-		path: '/moderation',
-		name: 'moderation',
-		component: page(() => import('./pages/admin/moderation.vue')),
-	}, {
-		path: '/email-settings',
-		name: 'email-settings',
-		component: page(() => import('./pages/admin/email-settings.vue')),
-	}, {
-		path: '/object-storage',
-		name: 'object-storage',
-		component: page(() => import('./pages/admin/object-storage.vue')),
-	}, {
-		path: '/security',
-		name: 'security',
-		component: page(() => import('./pages/admin/security.vue')),
-	}, {
-		path: '/relays',
-		name: 'relays',
-		component: page(() => import('./pages/admin/relays.vue')),
-	}, {
-		path: '/instance-block',
-		name: 'instance-block',
-		component: page(() => import('./pages/admin/instance-block.vue')),
-	}, {
-		path: '/proxy-account',
-		name: 'proxy-account',
-		component: page(() => import('./pages/admin/proxy-account.vue')),
-	}, {
-		path: '/external-services',
-		name: 'external-services',
-		component: page(() => import('./pages/admin/external-services.vue')),
-	}, {
-		path: '/other-settings',
-		name: 'other-settings',
-		component: page(() => import('./pages/admin/other-settings.vue')),
-	}, {
-		path: '/server-rules',
-		name: 'server-rules',
-		component: page(() => import('./pages/admin/server-rules.vue')),
-	}, {
-		path: '/invites',
-		name: 'invites',
-		component: page(() => import('./pages/admin/invites.vue')),
-	}, {
-		path: '/',
-		component: page(() => import('./pages/_empty_.vue')),
-	}],
-}, {
-	path: '/my/notifications',
-	component: page(() => import('./pages/notifications.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/favorites',
-	component: page(() => import('./pages/favorites.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/achievements',
-	component: page(() => import('./pages/achievements.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/drive/folder/:folder',
-	component: page(() => import('./pages/drive.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/drive',
-	component: page(() => import('./pages/drive.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/drive/file/:fileId',
-	component: page(() => import('./pages/drive.file.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/follow-requests',
-	component: page(() => import('./pages/follow-requests.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/lists/:listId',
-	component: page(() => import('./pages/my-lists/list.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/lists',
-	component: page(() => import('./pages/my-lists/index.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/clips',
-	component: page(() => import('./pages/my-clips/index.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/antennas/create',
-	component: page(() => import('./pages/my-antennas/create.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/antennas/:antennaId',
-	component: page(() => import('./pages/my-antennas/edit.vue')),
-	loginRequired: true,
-}, {
-	path: '/my/antennas',
-	component: page(() => import('./pages/my-antennas/index.vue')),
-	loginRequired: true,
-}, {
-	path: '/timeline/list/:listId',
-	component: page(() => import('./pages/user-list-timeline.vue')),
-	loginRequired: true,
-}, {
-	path: '/timeline/antenna/:antennaId',
-	component: page(() => import('./pages/antenna-timeline.vue')),
-	loginRequired: true,
-}, {
-	path: '/clicker',
-	component: page(() => import('./pages/clicker.vue')),
-	loginRequired: true,
-}, {
-	path: '/bubble-game',
-	component: page(() => import('./pages/drop-and-fusion.vue')),
-	loginRequired: true,
-}, {
-	path: '/timeline',
-	component: page(() => import('./pages/timeline.vue')),
-}, {
-	name: 'index',
-	path: '/',
-	component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')),
-	globalCacheKey: 'index',
-}, {
-	path: '/:(*)',
-	component: page(() => import('./pages/not-found.vue')),
-}];
-
-export const mainRouter = new Router(routes, location.pathname + location.search + location.hash, !!$i, page(() => import('@/pages/not-found.vue')));
-
-window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
-
-mainRouter.addListener('push', ctx => {
-	window.history.pushState({ key: ctx.key }, '', ctx.path);
-});
-
-window.addEventListener('popstate', (event) => {
-	mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
-});
-
-export function useRouter(): Router {
-	return inject<Router | null>('router', null) ?? mainRouter;
-}
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 2735253b36..d9a52c3741 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -13,11 +13,11 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore, userActions } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
-import { mainRouter } from '@/router.js';
-import { Router } from '@/nirax.js';
+import { IRouter } from '@/nirax.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
+import { mainRouter } from '@/global/router/main.js';
 
-export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) {
+export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
 
 	const cleanups = [] as (() => void)[];
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
index ff438af24f..ddcfd8852e 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/scripts/lookup.ts
@@ -6,8 +6,8 @@
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
 import { Router } from '@/nirax.js';
+import { mainRouter } from '@/global/router/main.js';
 
 export async function lookup(router?: Router) {
 	const _router = router ?? mainRouter;
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index 504484f8de..4c77465eb1 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -7,8 +7,8 @@ import { post } from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { $i, login } from '@/account.js';
 import { getAccountFromId } from '@/scripts/get-account-from-id.js';
-import { mainRouter } from '@/router.js';
 import { deepClone } from '@/scripts/clone.js';
+import { mainRouter } from '@/global/router/main.js';
 
 export function swInject() {
 	navigator.serviceWorker.addEventListener('message', async ev => {
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index e0985fdb11..fdddc0bb69 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -52,11 +52,11 @@ import XCommon from './_common_/common.vue';
 import { instanceName } from '@/config.js';
 import { StickySidebar } from '@/scripts/sticky-sidebar.js';
 import * as os from '@/os.js';
-import { mainRouter } from '@/router.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { mainRouter } from '@/global/router/main.js';
 const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index d184764b82..304ebbf0b2 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -103,7 +103,6 @@ import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { defaultStore } from '@/store.js';
@@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
 import XMentionsColumn from '@/ui/deck/mentions-column.vue';
 import XDirectColumn from '@/ui/deck/direct-column.vue';
 import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
+import { mainRouter } from '@/global/router/main.js';
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
 
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index c2b8f19079..674132e0d7 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -24,10 +24,10 @@ import XColumn from './column.vue';
 import { deckStore, Column } from '@/ui/deck/deck-store.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { getScrollContainer } from '@/scripts/scroll.js';
+import { mainRouter } from '@/global/router/main.js';
 
 defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index f32f2de3df..b0a2aa35f9 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { provide, ComputedRef, ref } from 'vue';
 import XCommon from './_common_/common.vue';
-import { mainRouter } from '@/router.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { instanceName } from '@/config.js';
+import { mainRouter } from '@/global/router/main.js';
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index f46f55d988..6f13f3fe87 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -105,12 +105,12 @@ import { defaultStore } from '@/store.js';
 import { navbarItemDef } from '@/navbar.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { mainRouter } from '@/router.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { CURRENT_STICKY_BOTTOM } from '@/const.js';
 import { useScrollPositionManager } from '@/nirax.js';
+import { mainRouter } from '@/global/router/main.js';
 
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 5af6bc30a8..d97c786d4a 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -79,10 +79,10 @@ import { instance } from '@/instance.js';
 import XSigninDialog from '@/components/MkSigninDialog.vue';
 import XSignupDialog from '@/components/MkSignupDialog.vue';
 import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { mainRouter } from '@/router.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
+import { mainRouter } from '@/global/router/main.js';
 
 const DESKTOP_THRESHOLD = 1100;
 
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index b819b6ca0a..957044c52b 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -24,10 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { provide, ComputedRef, ref } from 'vue';
 import XCommon from './_common_/common.vue';
-import { mainRouter } from '@/router.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { instanceName, ui } from '@/config.js';
 import { i18n } from '@/i18n.js';
+import { mainRouter } from '@/global/router/main.js';
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();