From 6dd6fcf88f621d787c6524d81844b1d514434859 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 14 Jul 2024 14:49:50 +0900
Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E3=82=B5=E3=83=BC?=
 =?UTF-8?q?=E3=83=90=E3=83=BC=E6=83=85=E5=A0=B1=E3=83=BB=E3=81=8A=E5=95=8F?=
 =?UTF-8?q?=E3=81=84=E5=90=88=E3=82=8F=E3=81=9B=E3=83=9A=E3=83=BC=E3=82=B8?=
 =?UTF-8?q?=E3=82=92=E6=94=B9=E4=BF=AE=20(#14198)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* improve(frontend): サーバー情報・お問い合わせページを改修 (#238)

* Revert "Revert "enhance(frontend): add contact page" (#208)" (This reverts commit 5a329a09c987b3249f97f9d53af67d1bffb09eea.)

* improve(frontend): サーバー情報・お問い合わせページを改修

(cherry picked from commit e72758d8cda3db009c5d1bf1f4141682931b91f8)

* fix

* Update Changelog

* tweak

* lint

* 既存の翻訳を使用するように

---------

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
---
 CHANGELOG.md                                  |   2 +
 packages/frontend/src/components/MkMenu.vue   |   1 +
 .../src/components/MkVisitorDashboard.vue     |  11 +-
 .../frontend/src/pages/about.overview.vue     | 205 ++++++++++++++++++
 packages/frontend/src/pages/about.vue         | 201 +----------------
 packages/frontend/src/pages/contact.vue       |  26 ++-
 packages/frontend/src/ui/_common_/common.ts   |  24 +-
 7 files changed, 250 insertions(+), 220 deletions(-)
 create mode 100644 packages/frontend/src/pages/about.overview.vue

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcc2aa29c..6e411e532 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@
 - Enhance: 非ログイン時のハイライトTLのデザインを改善
 - Enhance: フロントエンドのアクセシビリティ改善  
   (Based on https://github.com/taiyme/misskey/pull/226)
+- Enhance: サーバー情報ページ・お問い合わせページを改善  
+  (Cherry-picked from https://github.com/taiyme/misskey/pull/238)
 - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
 - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
 - Fix: リバーシの対局を正しく共有できないことがある問題を修正
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 68479989b..2276da1d2 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				:class="['_button', $style.item]"
 				:href="item.href"
 				:target="item.target"
+				:rel="item.target === '_blank' ? 'noopener noreferrer' : undefined"
 				:download="item.download"
 				@click.passive="close(true)"
 				@mouseenter.passive="onItemMouseEnter"
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 4d81bd028..445780eca 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div class="_gaps_s" :class="$style.mainActions">
 				<MkButton :class="$style.mainAction" full rounded gradate data-cy-signup style="margin-right: 12px;" @click="signup()">{{ i18n.ts.joinThisServer }}</MkButton>
-				<MkButton :class="$style.mainAction" full rounded @click="exploreOtherServers()">{{ i18n.ts.exploreOtherServers }}</MkButton>
+				<MkButton :class="$style.mainAction" full rounded link to="https://misskey-hub.net/servers/">{{ i18n.ts.exploreOtherServers }}</MkButton>
 				<MkButton :class="$style.mainAction" full rounded data-cy-signin @click="signin()">{{ i18n.ts.login }}</MkButton>
 			</div>
 		</div>
@@ -65,7 +65,8 @@ import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import MkNumber from '@/components/MkNumber.vue';
 import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
-import { openInstanceMenu } from '@/ui/_common_/common';
+import { openInstanceMenu } from '@/ui/_common_/common.js';
+import type { MenuItem } from '@/types/menu.js';
 
 const stats = ref<Misskey.entities.StatsResponse | null>(null);
 
@@ -89,13 +90,9 @@ function signup() {
 	});
 }
 
-function showMenu(ev) {
+function showMenu(ev: MouseEvent) {
 	openInstanceMenu(ev);
 }
-
-function exploreOtherServers() {
-	window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
-}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
new file mode 100644
index 000000000..84419b3be
--- /dev/null
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -0,0 +1,205 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m">
+	<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
+		<div style="overflow: clip;">
+			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
+			<div :class="$style.bannerName">
+				<b>{{ instance.name ?? host }}</b>
+			</div>
+		</div>
+	</div>
+
+	<MkKeyValue>
+		<template #key>{{ i18n.ts.description }}</template>
+		<template #value><div v-html="instance.description"></div></template>
+	</MkKeyValue>
+
+	<FormSection>
+		<div class="_gaps_m">
+			<MkKeyValue :copy="version">
+				<template #key>Misskey</template>
+				<template #value>{{ version }}</template>
+			</MkKeyValue>
+			<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
+			</div>
+			<FormLink to="/about-misskey">
+				<template #icon><i class="ti ti-info-circle"></i></template>
+				{{ i18n.ts.aboutMisskey }}
+			</FormLink>
+			<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
+				<template #icon><i class="ti ti-code"></i></template>
+				{{ i18n.ts.sourceCode }}
+			</FormLink>
+			<MkInfo v-else warn>
+				{{ i18n.ts.sourceCodeIsNotYetProvided }}
+			</MkInfo>
+		</div>
+	</FormSection>
+
+	<FormSection>
+		<div class="_gaps_m">
+			<FormSplit>
+				<MkKeyValue :copy="instance.maintainerName">
+					<template #key>{{ i18n.ts.administrator }}</template>
+					<template #value>
+						<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
+						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+					</template>
+				</MkKeyValue>
+				<MkKeyValue :copy="instance.maintainerEmail">
+					<template #key>{{ i18n.ts.contact }}</template>
+					<template #value>
+						<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
+						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+					</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.inquiry }}</template>
+					<template #value>
+						<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+					</template>
+				</MkKeyValue>
+			</FormSplit>
+			<div class="_gaps_s">
+				<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
+					<template #icon><i class="ti ti-user-shield"></i></template>
+					<template #default>{{ i18n.ts.impressum }}</template>
+				</FormLink>
+				<MkFolder v-if="instance.serverRules.length > 0">
+					<template #icon><i class="ti ti-checkup-list"></i></template>
+					<template #label>{{ i18n.ts.serverRules }}</template>
+					<ol class="_gaps_s" :class="$style.rules">
+						<li v-for="item in instance.serverRules" :key="item" :class="$style.rule">
+							<div :class="$style.ruleText" v-html="item"></div>
+						</li>
+					</ol>
+				</MkFolder>
+				<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
+					<template #icon><i class="ti ti-license"></i></template>
+					<template #default>{{ i18n.ts.termsOfService }}</template>
+				</FormLink>
+				<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
+					<template #icon><i class="ti ti-shield-lock"></i></template>
+					<template #default>{{ i18n.ts.privacyPolicy }}</template>
+				</FormLink>
+				<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
+					<template #icon><i class="ti ti-message"></i></template>
+					<template #default>{{ i18n.ts.feedback }}</template>
+				</FormLink>
+			</div>
+		</div>
+	</FormSection>
+
+	<FormSuspense v-slot="{ result: stats }" :p="initStats">
+		<FormSection>
+			<template #label>{{ i18n.ts.statistics }}</template>
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.users }}</template>
+					<template #value>{{ number(stats.originalUsersCount) }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.notes }}</template>
+					<template #value>{{ number(stats.originalNotesCount) }}</template>
+				</MkKeyValue>
+			</FormSplit>
+		</FormSection>
+	</FormSuspense>
+
+	<FormSection>
+		<template #label>Well-known resources</template>
+		<div class="_gaps_s">
+			<FormLink to="/.well-known/host-meta" external>host-meta</FormLink>
+			<FormLink to="/.well-known/host-meta.json" external>host-meta.json</FormLink>
+			<FormLink to="/.well-known/nodeinfo" external>nodeinfo</FormLink>
+			<FormLink to="/robots.txt" external>robots.txt</FormLink>
+			<FormLink to="/manifest.json" external>manifest.json</FormLink>
+		</div>
+	</FormSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { host, version } from '@/config.js';
+import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import number from '@/filters/number.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import FormLink from '@/components/form/link.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSplit from '@/components/form/split.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkLink from '@/components/MkLink.vue';
+
+const initStats = () => misskeyApi('stats', {});
+</script>
+
+<style lang="scss" module>
+.banner {
+	text-align: center;
+	border-radius: 10px;
+	overflow: clip;
+	background-color: var(--panel);
+	background-size: cover;
+	background-position: center center;
+}
+
+.bannerIcon {
+	display: block;
+	margin: 16px auto 0 auto;
+	height: 64px;
+	border-radius: 8px;
+}
+
+.bannerName {
+	display: block;
+	padding: 16px;
+	color: #fff;
+	text-shadow: 0 0 8px #000;
+	background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
+}
+
+.rules {
+	counter-reset: item;
+	list-style: none;
+	padding: 0;
+	margin: 0;
+}
+
+.rule {
+	display: flex;
+	gap: 8px;
+	word-break: break-word;
+
+	&::before {
+		flex-shrink: 0;
+		display: flex;
+		position: sticky;
+		top: calc(var(--stickyTop, 0px) + 8px);
+		counter-increment: item;
+		content: counter(item);
+		width: 32px;
+		height: 32px;
+		line-height: 32px;
+		background-color: var(--accentedBg);
+		color: var(--accent);
+		font-size: 13px;
+		font-weight: bold;
+		align-items: center;
+		justify-content: center;
+		border-radius: 999px;
+	}
+}
+
+.ruleText {
+	padding-top: 6px;
+}
+</style>
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 324d1c11d..8dfeb6d2a 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -8,113 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
 		<MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
-			<div class="_gaps_m">
-				<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
-					<div style="overflow: clip;">
-						<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
-						<div :class="$style.bannerName">
-							<b>{{ instance.name ?? host }}</b>
-						</div>
-					</div>
-				</div>
-
-				<MkKeyValue>
-					<template #key>{{ i18n.ts.description }}</template>
-					<template #value><div v-html="instance.description"></div></template>
-				</MkKeyValue>
-
-				<FormSection>
-					<div class="_gaps_m">
-						<MkKeyValue :copy="version">
-							<template #key>Misskey</template>
-							<template #value>{{ version }}</template>
-						</MkKeyValue>
-						<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
-						</div>
-						<FormLink to="/about-misskey">
-							<template #icon><i class="ti ti-info-circle"></i></template>
-							{{ i18n.ts.aboutMisskey }}
-						</FormLink>
-						<FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
-							<template #icon><i class="ti ti-code"></i></template>
-							{{ i18n.ts.sourceCode }}
-						</FormLink>
-						<MkInfo v-else warn>
-							{{ i18n.ts.sourceCodeIsNotYetProvided }}
-						</MkInfo>
-					</div>
-				</FormSection>
-
-				<FormSection>
-					<div class="_gaps_m">
-						<FormSplit>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.administrator }}</template>
-								<template #value>{{ instance.maintainerName }}</template>
-							</MkKeyValue>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.contact }}</template>
-								<template #value>{{ instance.maintainerEmail }}</template>
-							</MkKeyValue>
-						</FormSplit>
-						<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
-							<template #icon><i class="ti ti-user-shield"></i></template>
-							{{ i18n.ts.impressum }}
-						</FormLink>
-						<div class="_gaps_s">
-							<MkFolder v-if="instance.serverRules.length > 0">
-								<template #label>
-									<i class="ti ti-checkup-list"></i>
-									{{ i18n.ts.serverRules }}
-								</template>
-
-								<ol class="_gaps_s" :class="$style.rules">
-									<li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
-								</ol>
-							</MkFolder>
-							<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
-								<template #icon><i class="ti ti-license"></i></template>
-								{{ i18n.ts.termsOfService }}
-							</FormLink>
-							<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
-								<template #icon><i class="ti ti-shield-lock"></i></template>
-								{{ i18n.ts.privacyPolicy }}
-							</FormLink>
-							<FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
-								<template #icon><i class="ti ti-message"></i></template>
-								{{ i18n.ts.feedback }}
-							</FormLink>
-						</div>
-					</div>
-				</FormSection>
-
-				<FormSuspense :p="initStats">
-					<FormSection>
-						<template #label>{{ i18n.ts.statistics }}</template>
-						<FormSplit>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.users }}</template>
-								<template #value>{{ number(stats.originalUsersCount) }}</template>
-							</MkKeyValue>
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.notes }}</template>
-								<template #value>{{ number(stats.originalNotesCount) }}</template>
-							</MkKeyValue>
-						</FormSplit>
-					</FormSection>
-				</FormSuspense>
-
-				<FormSection>
-					<template #label>Well-known resources</template>
-					<div class="_gaps_s">
-						<FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
-						<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
-						<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
-						<FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
-						<FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
-					</div>
-				</FormSection>
-			</div>
+			<XOverview/>
 		</MkSpacer>
 		<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
 			<XEmojis/>
@@ -130,26 +24,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import XEmojis from './about.emojis.vue';
-import XFederation from './about.federation.vue';
-import { version, host } from '@/config.js';
-import FormLink from '@/components/form/link.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSuspense from '@/components/form/suspense.vue';
-import FormSplit from '@/components/form/split.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import MkKeyValue from '@/components/MkKeyValue.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkInstanceStats from '@/components/MkInstanceStats.vue';
-import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import number from '@/filters/number.js';
+import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { claimAchievement } from '@/scripts/achievements.js';
-import { instance } from '@/instance.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
+
+const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
+const XEmojis = defineAsyncComponent(() => import('@/pages/about.emojis.vue'));
+const XFederation = defineAsyncComponent(() => import('@/pages/about.federation.vue'));
+const MkInstanceStats = defineAsyncComponent(() => import('@/components/MkInstanceStats.vue'));
 
 const props = withDefaults(defineProps<{
 	initialTab?: string;
@@ -157,7 +41,6 @@ const props = withDefaults(defineProps<{
 	initialTab: 'overview',
 });
 
-const stats = ref<Misskey.entities.StatsResponse | null>(null);
 const tab = ref(props.initialTab);
 
 watch(tab, () => {
@@ -166,11 +49,6 @@ watch(tab, () => {
 	}
 });
 
-const initStats = () => misskeyApi('stats', {
-}).then((res) => {
-	stats.value = res;
-});
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => [{
@@ -195,64 +73,3 @@ definePageMetadata(() => ({
 	icon: 'ti ti-info-circle',
 }));
 </script>
-
-<style lang="scss" module>
-.banner {
-	text-align: center;
-	border-radius: 10px;
-	overflow: clip;
-	background-size: cover;
-	background-position: center center;
-}
-
-.bannerIcon {
-	display: block;
-	margin: 16px auto 0 auto;
-	height: 64px;
-	border-radius: 8px;
-}
-
-.bannerName {
-	display: block;
-	padding: 16px;
-	color: #fff;
-	text-shadow: 0 0 8px #000;
-	background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
-}
-
-.rules {
-	counter-reset: item;
-	list-style: none;
-	padding: 0;
-	margin: 0;
-}
-
-.rule {
-	display: flex;
-	gap: 8px;
-	word-break: break-word;
-
-	&::before {
-		flex-shrink: 0;
-		display: flex;
-		position: sticky;
-		top: calc(var(--stickyTop, 0px) + 8px);
-		counter-increment: item;
-		content: counter(item);
-		width: 32px;
-		height: 32px;
-		line-height: 32px;
-		background-color: var(--accentedBg);
-		color: var(--accent);
-		font-size: 13px;
-		font-weight: bold;
-		align-items: center;
-		justify-content: center;
-		border-radius: 999px;
-	}
-}
-
-.ruleText {
-	padding-top: 6px;
-}
-</style>
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
index bcdcf4327..1f2bee5a7 100644
--- a/packages/frontend/src/pages/contact.vue
+++ b/packages/frontend/src/pages/contact.vue
@@ -7,18 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><MkPageHeader/></template>
 	<MkSpacer :contentMax="600" :marginMin="20">
-		<div class="_gaps">
-			<MkKeyValue>
-				<template #key>{{ i18n.ts.inquiry }}</template>
+		<div class="_gaps_m">
+			<MkKeyValue :copy="instance.maintainerName">
+				<template #key>{{ i18n.ts.administrator }}</template>
 				<template #value>
-					<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+					<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
+					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
-
-			<MkKeyValue>
-				<template #key>{{ i18n.ts.email }}</template>
+			<MkKeyValue :copy="instance.maintainerEmail">
+				<template #key>{{ i18n.ts.contact }}</template>
 				<template #value>
-					<div>{{ instance.maintainerEmail }}</div>
+					<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
+					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
+				</template>
+			</MkKeyValue>
+			<MkKeyValue :copy="instance.inquiryUrl">
+				<template #key>{{ i18n.ts.inquiry }}</template>
+				<template #value>
+					<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
 		</div>
@@ -28,8 +36,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { instance } from '@/instance.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkLink from '@/components/MkLink.vue';
 
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 20a280f68..74c302874 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -85,29 +85,29 @@ export function openInstanceMenu(ev: MouseEvent) {
 		icon: 'ti ti-help-circle',
 		to: '/contact',
 	}, (instance.impressumUrl) ? {
+		type: 'a',
 		text: i18n.ts.impressum,
 		icon: 'ti ti-file-invoice',
-		action: () => {
-			window.open(instance.impressumUrl, '_blank', 'noopener');
-		},
+		href: instance.impressumUrl,
+		target: '_blank',
 	} : undefined, (instance.tosUrl) ? {
+		type: 'a',
 		text: i18n.ts.termsOfService,
 		icon: 'ti ti-notebook',
-		action: () => {
-			window.open(instance.tosUrl, '_blank', 'noopener');
-		},
+		href: instance.tosUrl,
+		target: '_blank',
 	} : undefined, (instance.privacyPolicyUrl) ? {
+		type: 'a',
 		text: i18n.ts.privacyPolicy,
 		icon: 'ti ti-shield-lock',
-		action: () => {
-			window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
-		},
+		href: instance.privacyPolicyUrl,
+		target: '_blank',
 	} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
+		type: 'a',
 		text: i18n.ts.document,
 		icon: 'ti ti-bulb',
-		action: () => {
-			window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
-		},
+		href: 'https://misskey-hub.net/docs/for-users/',
+		target: '_blank',
 	}, ($i) ? {
 		text: i18n.ts._initialTutorial.launchTutorial,
 		icon: 'ti ti-presentation',