diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 25e56d2b8df3ae44336b05679d57c892a33a8d27..3f6ae27b890379f75aa8d53dcbc20c3ab077700d 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -5,10 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkStickyContainer>
-	<template #header>
-		<MkPageHeader/>
-	</template>
-	<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
+	<template #header><MkPageHeader/></template>
+	<MkSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<div :class="$style.text">
@@ -16,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ i18n.ts.nothing }}
 			</div>
 		</div>
-	</MKSpacer>
+	</MkSpacer>
 	<MkSpacer v-else :contentMax="800">
 		<div class="_gaps_m" style="text-align: center;">
 			<div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 48bc568ac4eb98c3af2d5f668e470166eb73984c..0ff1854154635604750c2d70bf597922da39425f 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
-	<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
+	<MkSpacer v-if="error != null" :contentMax="1200">
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ i18n.ts.nothing }}
 			</p>
 		</div>
-	</MKSpacer>
+	</MkSpacer>
 	<MkSpacer v-else-if="list" :contentMax="700" :class="$style.main">
 		<div v-if="list" class="members _margin">
 			<div :class="$style.member_text">{{ i18n.ts.members }}</div>
@@ -50,7 +50,7 @@ const props = defineProps<{
 }>();
 
 const list = ref<Misskey.entities.UserList | null>(null);
-const error = ref();
+const error = ref<unknown | null>(null);
 const users = ref<Misskey.entities.UserDetailed[]>([]);
 
 function fetchList(): void {
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 45f8ef21ed00e4a9e1851a11b9e352574fb19a42..46e510b49b522eaa946f8ad4400862bacd60510d 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <MkStickyContainer>
 	<template #header><MkPageHeader v-model:tab="tab" :tabs="headerTabs"/></template>
-	<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
+	<MkSpacer v-if="error != null" :contentMax="1200">
 		<div :class="$style.root">
 			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				{{ error }}
 			</p>
 		</div>
-	</MKSpacer>
+	</MkSpacer>
 	<MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
 		<div class="_gaps_s">
 			<div v-if="role">{{ role.description }}</div>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</MkSpacer>
 	<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
-		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/>
+		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.roleId"/>
 		<div v-else-if="!visible" class="_fullinfo">
 			<img :src="infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.nothing }}</div>
@@ -47,23 +47,24 @@ import { instanceName } from '@@/js/config.js';
 import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
-	role: string;
+	roleId: string;
 	initialTab?: string;
 }>(), {
 	initialTab: 'users',
 });
 
+// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 const tab = ref(props.initialTab);
-const role = ref<Misskey.entities.Role>();
-const error = ref();
+const role = ref<Misskey.entities.Role | null>(null);
+const error = ref<string | null>(null);
 const visible = ref(false);
 
-watch(() => props.role, () => {
+watch(() => props.roleId, () => {
 	misskeyApi('roles/show', {
-		roleId: props.role,
+		roleId: props.roleId,
 	}).then(res => {
 		role.value = res;
-		document.title = `${role.value.name} | ${instanceName}`;
+		error.value = null;
 		visible.value = res.isExplorable && res.isPublic;
 	}).catch((err) => {
 		if (err.code === 'NO_SUCH_ROLE') {
@@ -71,7 +72,6 @@ watch(() => props.role, () => {
 		} else {
 			error.value = i18n.ts.somethingHappened;
 		}
-		document.title = `${error.value} | ${instanceName}`;
 	});
 }, { immediate: true });
 
@@ -79,7 +79,7 @@ const users = computed(() => ({
 	endpoint: 'roles/users' as const,
 	limit: 30,
 	params: {
-		roleId: props.role,
+		roleId: props.roleId,
 	},
 }));
 
@@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
 }]);
 
 definePageMetadata(() => ({
-	title: role.value ? role.value.name : i18n.ts.role,
+	title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
 	icon: 'ti ti-badge',
 }));
 </script>
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 75f994b865178e7585e756d32b7f05d90f972685..b5fd6b6ec321b068584959fffa55bae9e8b381fb 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -217,7 +217,7 @@ const routes: RouteDef[] = [{
 	component: page(() => import('@/pages/theme-editor.vue')),
 	loginRequired: true,
 }, {
-	path: '/roles/:role',
+	path: '/roles/:roleId',
 	component: page(() => import('@/pages/role.vue')),
 }, {
 	path: '/user-tags/:tag',