From aab1c769814b08c257cad3025422a0eea3bfba4f Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 1 Sep 2024 08:44:55 +0900
Subject: [PATCH] wip

---
 .../src/components/EmCustomEmoji.vue          |  5 +-
 .../src/components/EmInstanceTicker.vue       | 17 ++---
 .../src/components/EmNoteDetailed.vue         |  2 +-
 packages/frontend-embed/src/pages/clip.vue    |  2 +-
 packages/frontend-embed/src/pages/tag.vue     |  2 +-
 .../src/pages/user-timeline.vue               |  2 +-
 .../src/to-be-shared/media-proxy.ts           | 53 ----------------
 packages/frontend-shared/js/const.ts          |  4 --
 packages/frontend-shared/js/media-proxy.ts    | 62 +++++++++++++++++++
 packages/frontend/src/boot/common.ts          | 33 +++++-----
 packages/frontend/src/boot/main-boot.ts       |  5 +-
 .../frontend/src/components/MkChannelList.vue |  6 +-
 .../src/components/MkCropperDialog.vue        |  7 ++-
 .../src/components/MkDonation.stories.impl.ts |  2 +-
 .../frontend/src/components/MkDonation.vue    |  6 +-
 .../src/components/MkFeaturedPhotos.vue       |  5 +-
 .../src/components/MkForgotPassword.vue       |  7 ++-
 .../frontend/src/components/MkFormDialog.vue  |  5 +-
 .../src/components/MkInstanceCardMini.vue     |  7 ++-
 .../src/components/MkInstanceTicker.vue       |  8 +--
 packages/frontend/src/components/MkLink.vue   |  7 ++-
 .../frontend/src/components/MkMediaImage.vue  |  7 ++-
 .../frontend/src/components/MkMention.vue     |  7 ++-
 packages/frontend/src/components/MkNote.vue   |  9 +--
 .../src/components/MkNoteDetailed.vue         |  9 +--
 packages/frontend/src/components/MkNotes.vue  |  5 +-
 .../src/components/MkNotification.vue         |  3 +-
 .../src/components/MkNotifications.vue        |  5 +-
 .../frontend/src/components/MkPagination.vue  |  5 +-
 .../frontend/src/components/MkPostForm.vue    |  5 +-
 .../MkPushNotificationAllowButton.vue         | 11 ++--
 .../src/components/MkSignupDialog.form.vue    | 29 ++++-----
 .../MkSignupDialog.rules.stories.impl.ts      |  2 +-
 .../src/components/MkSignupDialog.rules.vue   | 19 +++---
 .../components/MkSourceCodeAvailablePopup.vue |  8 ++-
 .../frontend/src/components/MkTimeline.vue    |  5 +-
 .../src/components/MkTutorialDialog.vue       |  7 ++-
 .../frontend/src/components/MkUserInfo.vue    |  6 +-
 .../frontend/src/components/MkUserList.vue    |  6 +-
 .../frontend/src/components/MkUserPopup.vue   | 10 +--
 .../src/components/MkUserSetupDialog.vue      | 12 ++--
 .../src/components/MkVisitorDashboard.vue     | 16 ++---
 .../frontend/src/components/global/MkAd.vue   |  9 +--
 .../src/components/global/MkAvatar.vue        |  9 +--
 .../src/components/global/MkCustomEmoji.vue   |  7 ++-
 .../src/components/global/MkError.vue         |  6 +-
 .../frontend/src/components/global/MkUrl.vue  |  7 ++-
 .../src/components/page/page.text.vue         |  7 ++-
 packages/frontend/src/const.ts                |  6 +-
 packages/frontend/src/local-storage.ts        |  1 -
 packages/frontend/src/pages/_error_.vue       |  7 ++-
 packages/frontend/src/pages/about-misskey.vue | 15 ++---
 .../frontend/src/pages/about.overview.vue     | 38 ++++++------
 .../src/pages/admin/bot-protection.vue        |  4 +-
 .../frontend/src/pages/admin/branding.vue     |  4 +-
 .../src/pages/admin/email-settings.vue        |  7 ++-
 .../src/pages/admin/external-services.vue     |  4 +-
 packages/frontend/src/pages/admin/index.vue   | 17 ++---
 .../src/pages/admin/instance-block.vue        |  4 +-
 .../frontend/src/pages/admin/moderation.vue   |  4 +-
 .../src/pages/admin/object-storage.vue        |  4 +-
 .../src/pages/admin/other-settings.vue        |  4 +-
 .../src/pages/admin/proxy-account.vue         |  4 +-
 .../frontend/src/pages/admin/roles.editor.vue |  7 ++-
 .../frontend/src/pages/admin/roles.role.vue   |  5 +-
 packages/frontend/src/pages/admin/roles.vue   |  4 +-
 .../frontend/src/pages/admin/security.vue     |  4 +-
 .../frontend/src/pages/admin/server-rules.vue | 10 +--
 .../frontend/src/pages/admin/settings.vue     | 10 +--
 packages/frontend/src/pages/ads.vue           |  6 +-
 packages/frontend/src/pages/contact.vue       | 16 ++---
 .../frontend/src/pages/drive.file.info.vue    |  9 +--
 packages/frontend/src/pages/favorites.vue     |  5 +-
 .../frontend/src/pages/follow-requests.vue    |  5 +-
 packages/frontend/src/pages/instance-info.vue |  7 ++-
 packages/frontend/src/pages/invite.vue        | 13 ++--
 packages/frontend/src/pages/list.vue          |  7 ++-
 .../frontend/src/pages/my-antennas/index.vue  |  5 +-
 .../frontend/src/pages/my-lists/index.vue     |  5 +-
 packages/frontend/src/pages/not-found.vue     |  7 ++-
 packages/frontend/src/pages/page.vue          |  9 +--
 packages/frontend/src/pages/role.vue          |  9 +--
 packages/frontend/src/pages/search.note.vue   |  9 +--
 packages/frontend/src/pages/search.vue        |  8 ++-
 packages/frontend/src/pages/settings/apps.vue |  5 +-
 .../frontend/src/pages/settings/email.vue     |  9 +--
 .../frontend/src/pages/settings/index.vue     |  5 +-
 .../src/pages/settings/mute-block.vue         |  9 +--
 .../frontend/src/pages/settings/theme.vue     |  9 +--
 packages/frontend/src/pages/timeline.vue      | 18 +++---
 packages/frontend/src/pages/user/home.vue     | 12 ++--
 .../frontend/src/pages/user/index.files.vue   |  7 ++-
 .../frontend/src/pages/welcome.entrance.a.vue | 11 ++--
 packages/frontend/src/pages/welcome.vue       |  4 +-
 .../frontend/src/scripts/check-permissions.ts | 18 +++---
 packages/frontend/src/scripts/clear-cache.ts  |  4 +-
 .../frontend/src/scripts/get-note-menu.ts     | 21 +++----
 .../frontend/src/scripts/get-user-menu.ts     |  6 +-
 packages/frontend/src/scripts/media-proxy.ts  | 53 ----------------
 packages/frontend/src/scripts/upload.ts       | 22 +++----
 .../src/{instance.ts => server-metadata.ts}   | 34 +++-------
 packages/frontend/src/timelines.ts            | 12 ++--
 packages/frontend/src/ui/_common_/common.ts   | 23 +++----
 .../src/ui/_common_/navbar-for-mobile.vue     |  9 +--
 packages/frontend/src/ui/_common_/navbar.vue  | 11 ++--
 .../src/ui/_common_/statusbar-federation.vue  |  9 +--
 packages/frontend/src/ui/classic.header.vue   |  7 ++-
 packages/frontend/src/ui/classic.sidebar.vue  |  7 ++-
 packages/frontend/src/ui/deck/tl-column.vue   |  7 ++-
 packages/frontend/src/ui/visitor.vue          |  9 +--
 .../src/widgets/WidgetBirthdayFollowings.vue  |  5 +-
 .../frontend/src/widgets/WidgetFederation.vue |  9 +--
 .../src/widgets/WidgetInstanceCloud.vue       |  9 +--
 .../src/widgets/WidgetInstanceInfo.vue        | 10 +--
 .../frontend/src/widgets/WidgetPhotos.vue     |  7 ++-
 packages/frontend/src/widgets/WidgetRss.vue   |  5 +-
 .../frontend/src/widgets/WidgetTimeline.vue   |  8 ++-
 117 files changed, 587 insertions(+), 568 deletions(-)
 delete mode 100644 packages/frontend-embed/src/to-be-shared/media-proxy.ts
 create mode 100644 packages/frontend-shared/js/media-proxy.ts
 delete mode 100644 packages/frontend/src/scripts/media-proxy.ts
 rename packages/frontend/src/{instance.ts => server-metadata.ts} (51%)

diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue
index b4ebf96ebf..fa4f1862c8 100644
--- a/packages/frontend-embed/src/components/EmCustomEmoji.vue
+++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue
@@ -25,9 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, inject, ref } from 'vue';
-import { getProxiedImageUrl } from '@/to-be-shared/media-proxy.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 
+const mediaProxy = inject('mediaProxy');
+
 const props = defineProps<{
 	name: string;
 	normal?: boolean;
@@ -59,7 +60,7 @@ const url = computed(() => {
 	const proxied =
 		(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
 			? rawUrl.value
-			: getProxiedImageUrl(
+			: mediaProxy.getProxiedImageUrl(
 				rawUrl.value,
 				props.useOriginalSize ? undefined : 'emoji',
 				false,
diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue
index b4998c8894..85b3381085 100644
--- a/packages/frontend-embed/src/components/EmInstanceTicker.vue
+++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue
@@ -11,26 +11,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue';
-import { instanceName } from '@/config.js';
-import { instance as Instance } from '@/instance.js';
-import { getProxiedImageUrlNullable } from '@/to-be-shared/media-proxy.js';
+import { computed, inject } from 'vue';
+
+const mediaProxy = inject('mediaProxy');
 
 const props = defineProps<{
-	instance?: {
+	instance: {
 		faviconUrl?: string | null
 		name?: string | null
 		themeColor?: string | null
 	}
 }>();
 
-// if no instance data is given, this is for the local instance
-const instance = props.instance ?? {
-	name: instanceName,
-	themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
-};
-
-const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview'));
 
 const themeColor = instance.themeColor ?? '#777777';
 
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
index 2afb5d9fa7..cc9be13a57 100644
--- a/packages/frontend-embed/src/components/EmNoteDetailed.vue
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -142,7 +142,7 @@ import { userPage } from '@/utils.js';
 import { notePage } from '@/utils.js';
 import { i18n } from '@/i18n.js';
 import { shouldCollapsed } from '@/to-be-shared/collapsed.js';
-import { instance } from '@/instance.js';
+import { instance } from '@/server-metadata.js';
 import { url } from '@/config.js';
 import EmMfm from '@/components/EmMfm.js';
 
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
index 77fe95d883..bb4f8bf7c5 100644
--- a/packages/frontend-embed/src/pages/clip.vue
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -48,7 +48,7 @@ import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+import { instance } from '@/server-metadata.js';
 import { url, instanceName } from '@/config.js';
 import { scrollToTop } from '@@/js/scroll.js';
 import { isLink } from '@/scripts/is-link.js';
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
index e27f52c558..4e1b501421 100644
--- a/packages/frontend-embed/src/pages/tag.vue
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -45,7 +45,7 @@ import EmNotes from '@/components/EmNotes.vue';
 import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+import { instance } from '@/server-metadata.js';
 import { url, instanceName } from '@/config.js';
 import { scrollToTop } from '@@/js/scroll.js';
 import { isLink } from '@/scripts/is-link.js';
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
index b1ab77b990..13fbd442a2 100644
--- a/packages/frontend-embed/src/pages/user-timeline.vue
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -62,7 +62,7 @@ import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
 import { misskeyApi } from '@/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+import { instance } from '@/server-metadata.js';
 import { url, instanceName } from '@/config.js';
 import { defaultEmbedParams } from '@/embed-page.js';
 
diff --git a/packages/frontend-embed/src/to-be-shared/media-proxy.ts b/packages/frontend-embed/src/to-be-shared/media-proxy.ts
deleted file mode 100644
index facd50ac41..0000000000
--- a/packages/frontend-embed/src/to-be-shared/media-proxy.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { query } from '@@/js/url.js';
-import { url } from '@/config.js';
-import { instance } from '@/instance.js';
-
-export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
-	const localProxy = `${url}/proxy`;
-
-	if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
-		// もう既にproxyっぽそうだったらurlを取り出す
-		imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
-	}
-
-	return `${mustOrigin ? localProxy : instance.mediaProxy}/${
-		type === 'preview' ? 'preview.webp'
-		: 'image.webp'
-	}?${query({
-		url: imageUrl,
-		...(!noFallback ? { 'fallback': '1' } : {}),
-		...(type ? { [type]: '1' } : {}),
-		...(mustOrigin ? { origin: '1' } : {}),
-	})}`;
-}
-
-export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
-	if (imageUrl == null) return null;
-	return getProxiedImageUrl(imageUrl, type);
-}
-
-export function getStaticImageUrl(baseUrl: string): string {
-	const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
-
-	if (u.href.startsWith(`${url}/emoji/`)) {
-		// もう既にemojiっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
-	}
-
-	if (u.href.startsWith(instance.mediaProxy + '/')) {
-		// もう既にproxyっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
-	}
-
-	return `${instance.mediaProxy}/static.webp?${query({
-		url: u.href,
-		static: '1',
-	})}`;
-}
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index 8391fb638c..7f56f673c4 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -106,10 +106,6 @@ export const ROLE_POLICIES = [
 export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
 export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
 
-export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
-export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
-export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
-
 export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
 export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
 	tada: ['speed=', 'delay='],
diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts
new file mode 100644
index 0000000000..fcfbac61dc
--- /dev/null
+++ b/packages/frontend-shared/js/media-proxy.ts
@@ -0,0 +1,62 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { query } from '@@/js/url.js';
+import * as Misskey from 'misskey-js';
+
+export class MediaProxy {
+	private serverMetadata: Misskey.entities.MetaDetailed;
+	private url: string;
+
+	constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) {
+		this.serverMetadata = serverMetadata;
+		this.url = url;
+	}
+
+	public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
+		const localProxy = `${this.url}/proxy`;
+
+		if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
+			// もう既にproxyっぽそうだったらurlを取り出す
+			imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
+		}
+
+		return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${
+			type === 'preview' ? 'preview.webp'
+			: 'image.webp'
+		}?${query({
+			url: imageUrl,
+			...(!noFallback ? { 'fallback': '1' } : {}),
+			...(type ? { [type]: '1' } : {}),
+			...(mustOrigin ? { origin: '1' } : {}),
+		})}`;
+	}
+
+	public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
+		if (imageUrl == null) return null;
+		return this.getProxiedImageUrl(imageUrl, type);
+	}
+
+	public getStaticImageUrl(baseUrl: string): string {
+		const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url);
+
+		if (u.href.startsWith(`${this.url}/emoji/`)) {
+			// もう既にemojiっぽそうだったらsearchParams付けるだけ
+			u.searchParams.set('static', '1');
+			return u.href;
+		}
+
+		if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) {
+			// もう既にproxyっぽそうだったらsearchParams付けるだけ
+			u.searchParams.set('static', '1');
+			return u.href;
+		}
+
+		return `${this.serverMetadata.mediaProxy}/static.webp?${query({
+			url: u.href,
+			static: '1',
+		})}`;
+	}
+}
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 19d30f64ce..ad5a7c384f 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -5,16 +5,17 @@
 
 import { computed, watch, version as vueVersion, App } from 'vue';
 import { compareVersions } from 'compare-versions';
+import { MediaProxy } from '@@/js/media-proxy.js';
 import widgets from '@/widgets/index.js';
 import directives from '@/directives/index.js';
 import components from '@/components/index.js';
-import { version, lang, updateLocale, locale } from '@/config.js';
+import { version, lang, updateLocale, locale, url } from '@/config.js';
 import { applyTheme } from '@/scripts/theme.js';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
 import { updateI18n } from '@/i18n.js';
 import { $i, refreshAccount, login } from '@/account.js';
 import { defaultStore, ColdDeviceStorage } from '@/store.js';
-import { fetchInstance, instance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 import { reloadChannel } from '@/scripts/unison-reload.js';
 import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
@@ -119,11 +120,7 @@ export async function common(createVue: () => App<Element>) {
 	await defaultStore.ready;
 	await deckStore.ready;
 
-	const fetchInstanceMetaPromise = fetchInstance();
-
-	fetchInstanceMetaPromise.then(() => {
-		miLocalStorage.setItem('v', instance.version);
-	});
+	const serverMetadata = await fetchServerMetadata();
 
 	//#region loginId
 	const params = new URLSearchParams(location.search);
@@ -178,19 +175,17 @@ export async function common(createVue: () => App<Element>) {
 	});
 	//#endregion
 
-	fetchInstanceMetaPromise.then(() => {
-		if (defaultStore.state.themeInitial) {
-			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
-			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
-			defaultStore.set('themeInitial', false);
+	if (defaultStore.state.themeInitial) {
+		if (serverMetadata.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(serverMetadata.defaultLightTheme));
+		if (serverMetadata.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(serverMetadata.defaultDarkTheme));
+		defaultStore.set('themeInitial', false);
+	} else {
+		if (defaultStore.state.darkMode) {
+			applyTheme(darkTheme.value);
 		} else {
-			if (defaultStore.state.darkMode) {
-				applyTheme(darkTheme.value);
-			} else {
-				applyTheme(lightTheme.value);
-			}
+			applyTheme(lightTheme.value);
 		}
-	});
+	}
 
 	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
 		document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
@@ -239,6 +234,8 @@ export async function common(createVue: () => App<Element>) {
 	} catch (err) { /* empty */ }
 
 	const app = createVue();
+	app.provide('serverMetadata', serverMetadata);
+	app.provide('mediaProxy', new MediaProxy(serverMetadata, url));
 
 	setupRouter(app, createMainRouter);
 
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index b31281dcf2..2241177405 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -12,7 +12,6 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
 import * as sound from '@/scripts/sound.js';
 import { $i, signout, updateAccount } from '@/account.js';
-import { instance } from '@/instance.js';
 import { ColdDeviceStorage, defaultStore } from '@/store.js';
 import { reactionPicker } from '@/scripts/reaction-picker.js';
 import { miLocalStorage } from '@/local-storage.js';
@@ -23,6 +22,7 @@ import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mainRouter } from '@/router/main.js';
 import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
 import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => createApp(
@@ -267,8 +267,9 @@ export async function mainBoot() {
 			}
 		}
 
+		const serverMetadata = await fetchServerMetadata();
 		const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
-		if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
+		if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && serverMetadata.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
 			const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {
 				closed: () => dispose(),
 			});
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 2850ecca16..725bdc53e0 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkPagination :pagination="pagination">
 	<template #empty>
 		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.notFound }}</div>
 		</div>
 	</template>
@@ -19,10 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import MkChannelPreview from '@/components/MkChannelPreview.vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = withDefaults(defineProps<{
 	pagination: Paging;
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 54f6f39c9d..9ff8d32c2b 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, shallowRef, ref, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import Cropper from 'cropperjs';
 import tinycolor from 'tinycolor2';
@@ -41,7 +41,8 @@ import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import { apiUrl } from '@/config.js';
 import { i18n } from '@/i18n.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+
+const mediaProxy = inject('mediaProxy');
 
 const emit = defineEmits<{
 	(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
@@ -55,7 +56,7 @@ const props = defineProps<{
 	uploadFolder?: string | null;
 }>();
 
-const imgUrl = getProxiedImageUrl(props.file.url, undefined, true);
+const imgUrl = mediaProxy.getProxiedImageUrl(props.file.url, undefined, true);
 const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
 const imgEl = shallowRef<HTMLImageElement>();
 let cropper: Cropper | null = null;
diff --git a/packages/frontend/src/components/MkDonation.stories.impl.ts b/packages/frontend/src/components/MkDonation.stories.impl.ts
index 27d6b7df6c..94fca7a8fe 100644
--- a/packages/frontend/src/components/MkDonation.stories.impl.ts
+++ b/packages/frontend/src/components/MkDonation.stories.impl.ts
@@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions';
 import { StoryObj } from '@storybook/vue3';
 import { onBeforeUnmount } from 'vue';
 import MkDonation from './MkDonation.vue';
-import { instance } from '@/instance.js';
+import { instance } from '@/server-metadata.js';
 export const Default = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index 434fc81582..c81fa979d3 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.text">
 			<I18n :src="i18n.ts.pleaseDonate" tag="span">
 				<template #host>
-					{{ instance.name ?? host }}
+					{{ serverMetadata.name ?? host }}
 				</template>
 			</I18n>
 			<div style="margin-top: 0.2em;">
@@ -36,13 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkLink from '@/components/MkLink.vue';
 import { host } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const emit = defineEmits<{
 	(ev: 'closed'): void;
diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue
index c42c692db0..a3ef4e63ae 100644
--- a/packages/frontend/src/components/MkFeaturedPhotos.vue
+++ b/packages/frontend/src/components/MkFeaturedPhotos.vue
@@ -4,11 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="instance" :class="$style.root" :style="{ backgroundImage: `url(${ instance.backgroundImageUrl })` }"></div>
+<div :class="$style.root" :style="{ backgroundImage: `url(${ serverMetadata.backgroundImageUrl })` }"></div>
 </template>
 
 <script lang="ts" setup>
-import { instance } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 35112ad45d..0471b23e3b 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>{{ i18n.ts.forgotPassword }}</template>
 
 	<MkSpacer :marginMin="20" :marginMax="28">
-		<form v-if="instance.enableEmail" @submit.prevent="onSubmit">
+		<form v-if="serverMetadata.enableEmail" @submit.prevent="onSubmit">
 			<div class="_gaps_m">
 				<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
 					<template #label>{{ i18n.ts.username }}</template>
@@ -39,15 +39,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const emit = defineEmits<{
 	(ev: 'done'): void;
 	(ev: 'closed'): void;
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 124f114111..e2dc7c3b3d 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 		</div>
 		<div v-else class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 	</MkSpacer>
@@ -72,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { reactive, shallowRef } from 'vue';
+import { inject } from 'vue';
 import MkInput from './MkInput.vue';
 import MkTextarea from './MkTextarea.vue';
 import MkSwitch from './MkSwitch.vue';
@@ -83,7 +84,7 @@ import XFile from './MkFormDialog.file.vue';
 import type { Form } from '@/scripts/form.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
+const serverMetadata = inject('serverMetadata');
 
 const props = defineProps<{
 	title: string;
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index 17c974dd04..ab0abb6b0d 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -15,11 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+
+const mediaProxy = inject('mediaProxy');
 
 const props = defineProps<{
 	instance: Misskey.entities.FederationInstance;
@@ -34,7 +35,7 @@ misskeyApiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, spa
 });
 
 function getInstanceIcon(instance): string {
-	return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
+	return mediaProxy.getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? mediaProxy.getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
 }
 </script>
 
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 82c82199b5..b783ade5e9 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -11,10 +11,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, inject } from 'vue';
 import { instanceName } from '@/config.js';
-import { instance as Instance } from '@/instance.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+
+const mediaProxy = inject('mediaProxy');
 
 const props = defineProps<{
 	instance?: {
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
 	themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
 };
 
-const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
 
 const themeColor = instance.themeColor ?? '#777777';
 
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index e842ec2d6e..6ef1036cc7 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -15,13 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, inject, ref } from 'vue';
 import { url as local } from '@/config.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
 import * as os from '@/os.js';
-import { isEnabledUrlPreview } from '@/instance.js';
 import { MkABehavior } from '@/components/global/MkA.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = withDefaults(defineProps<{
 	url: string;
 	rel?: null | string;
@@ -35,7 +36,7 @@ const target = self ? null : '_blank';
 
 const el = ref<HTMLElement | { $el: HTMLElement }>();
 
-if (isEnabledUrlPreview.value) {
+if (serverMetadata.enableUrlPreview) {
 	useTooltip(el, (showing) => {
 		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 			showing,
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 0d1409e2c8..0c18c1c694 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -51,9 +51,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, ref, computed } from 'vue';
+import { watch, ref, computed, inject } from 'vue';
 import * as Misskey from 'misskey-js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import { defaultStore } from '@/store.js';
@@ -61,6 +60,8 @@ import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { $i, iAmModerator } from '@/account.js';
 
+const mediaProxy = inject('mediaProxy');
+
 const props = withDefaults(defineProps<{
 	image: Misskey.entities.DriveFile;
 	raw?: boolean;
@@ -79,7 +80,7 @@ const darkMode = ref<boolean>(defaultStore.state.darkMode);
 const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
 	? props.image.url
 	: defaultStore.state.disableShowingAnimatedImages
-		? getStaticImageUrl(props.image.url)
+		? mediaProxy.getStaticImageUrl(props.image.url)
 		: props.image.thumbnailUrl,
 );
 
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index bfb49a416e..62ec940ffb 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -15,14 +15,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { toUnicode } from 'punycode';
-import { computed } from 'vue';
+import { computed, inject } from 'vue';
 import tinycolor from 'tinycolor2';
 import { host as localHost } from '@/config.js';
 import { $i } from '@/account.js';
 import { defaultStore } from '@/store.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { MkABehavior } from '@/components/global/MkA.vue';
 
+const mediaProxy = inject('mediaProxy');
+
 const props = defineProps<{
 	username: string;
 	host: string;
@@ -42,7 +43,7 @@ bg.setAlpha(0.1);
 const bgCss = bg.toRgbString();
 
 const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
-	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
+	? mediaProxy.getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
 	: `/avatar/@${props.username}@${props.host}`,
 );
 </script>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 2927a46977..1465cf0b92 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
 					</div>
 					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
-					<div v-if="isEnabledUrlPreview">
+					<div v-if="serverMetadata.enableUrlPreview">
 						<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
 					</div>
 					<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
@@ -197,11 +197,12 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { host } from '@/config.js';
-import { isEnabledUrlPreview } from '@/instance.js';
 import { type Keymap } from '@/scripts/hotkey.js';
 import { focusPrev, focusNext } from '@/scripts/focus.js';
 import { getAppearNote } from '@/scripts/get-appear-note.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
 	pinned?: boolean;
@@ -523,7 +524,7 @@ function onContextmenu(ev: MouseEvent): void {
 		ev.preventDefault();
 		react();
 	} else {
-		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
+		const { menu, cleanup } = getNoteMenu({ serverMetadata }, { note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
 		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
@@ -533,7 +534,7 @@ function showMenu(): void {
 		return;
 	}
 
-	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
+	const { menu, cleanup } = getNoteMenu({ serverMetadata }, { note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
 	os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
 }
 
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 2b7d2afa04..d1269a177c 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
 				</div>
 				<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
-				<div v-if="isEnabledUrlPreview">
+				<div v-if="serverMetadata.enableUrlPreview">
 					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
 				</div>
 				<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
@@ -234,10 +234,11 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
-import { isEnabledUrlPreview } from '@/instance.js';
 import { getAppearNote } from '@/scripts/get-appear-note.js';
 import { type Keymap } from '@/scripts/hotkey.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
 	initialTab: string;
@@ -483,13 +484,13 @@ function onContextmenu(ev: MouseEvent): void {
 		ev.preventDefault();
 		react();
 	} else {
-		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
+		const { menu, cleanup } = getNoteMenu({ serverMetadata }, { note: note.value, translating, translation, isDeleted });
 		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
 function showMenu(): void {
-	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
+	const { menu, cleanup } = getNoteMenu({ serverMetadata }, { note: note.value, translating, translation, isDeleted });
 	os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup);
 }
 
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 0856c146ba..e7a94005cf 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
 	<template #empty>
 		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.noNotes }}</div>
 		</div>
 	</template>
@@ -33,11 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { shallowRef } from 'vue';
+import { inject } from 'vue';
 import MkNote from '@/components/MkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
+const serverMetadata = inject('serverMetadata');
 
 const props = defineProps<{
 	pagination: Paging;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index ee65743574..55cdda7285 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -148,7 +148,8 @@ import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { signinRequired } from '@/account.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 389987338d..26096359f3 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkPagination ref="pagingComponent" :pagination="pagination">
 		<template #empty>
 			<div class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 				<div>{{ i18n.ts.noNotifications }}</div>
 			</div>
 		</template>
@@ -32,7 +32,8 @@ import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { notificationTypes } from '@/const.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import { defaultStore } from '@/store.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import * as Misskey from 'misskey-js';
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index d30f915c55..6731a56fb4 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div v-else-if="empty" key="_empty_" class="empty">
 		<slot name="empty">
 			<div class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 				<div>{{ i18n.ts.nothing }}</div>
 			</div>
 		</slot>
@@ -90,7 +90,8 @@ function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): M
 
 </script>
 <script lang="ts" setup>
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import MkButton from '@/components/MkButton.vue';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index df251d9192..56bd05366f 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -120,7 +120,6 @@ import { selectFiles } from '@/scripts/select-file.js';
 import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js';
 import MkInfo from '@/components/MkInfo.vue';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { uploadFile } from '@/scripts/upload.js';
 import { deepClone } from '@/scripts/clone.js';
@@ -130,6 +129,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
 import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const $i = signinRequired();
 
 const modal = inject('modal');
@@ -249,7 +250,7 @@ const textLength = computed((): number => {
 });
 
 const maxTextLength = computed((): number => {
-	return instance ? instance.maxNoteTextLength : 1000;
+	return serverMetadata.maxNoteTextLength;
 });
 
 const canPost = computed((): boolean => {
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 5e42df4795..66b76bbd35 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -41,14 +41,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import { $i, getAccounts } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
-import { instance } from '@/instance.js';
 import { apiWithDialog, promiseDialog } from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
+const serverMetadata = inject('serverMetadata');
+
 defineProps<{
 	primary?: boolean;
 	gradate?: boolean;
@@ -72,12 +73,12 @@ const pushSubscription = ref<PushSubscription | null>(null);
 const pushRegistrationInServer = ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
 
 function subscribe() {
-	if (!registration.value || !supported.value || !instance.swPublickey) return;
+	if (!registration.value || !supported.value || !serverMetadata.swPublickey) return;
 
 	// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
 	return promiseDialog(registration.value.pushManager.subscribe({
 		userVisibleOnly: true,
-		applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
+		applicationServerKey: urlBase64ToUint8Array(serverMetadata.swPublickey),
 	})
 		.then(async subscription => {
 			pushSubscription.value = subscription;
@@ -156,7 +157,7 @@ if (navigator.serviceWorker == null) {
 
 		pushSubscription.value = await registration.value.pushManager.getSubscription();
 
-		if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
+		if (serverMetadata.swPublickey && ('PushManager' in window) && $i && $i.token) {
 			supported.value = true;
 
 			if (pushSubscription.value) {
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 5f08e416c1..3ce8e78fec 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<MkSpacer :marginMin="20" :marginMax="32">
 		<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
-			<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
+			<MkInput v-if="serverMetadata.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
 				<template #label>{{ i18n.ts.invitationCode }}</template>
 				<template #prefix><i class="ti ti-key"></i></template>
 			</MkInput>
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
 				</template>
 			</MkInput>
-			<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
+			<MkInput v-if="serverMetadata.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
 				<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
 				<template #prefix><i class="ti ti-mail"></i></template>
 				<template #caption>
@@ -62,10 +62,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
 				</template>
 			</MkInput>
-			<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
-			<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
-			<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
-			<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+			<MkCaptcha v-if="serverMetadata.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="serverMetadata.hcaptchaSiteKey"/>
+			<MkCaptcha v-if="serverMetadata.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="serverMetadata.mcaptchaSiteKey" :instanceUrl="serverMetadata.mcaptchaInstanceUrl"/>
+			<MkCaptcha v-if="serverMetadata.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="serverMetadata.recaptchaSiteKey"/>
+			<MkCaptcha v-if="serverMetadata.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="serverMetadata.turnstileSiteKey"/>
 			<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
 				<template v-if="submitting">
 					<MkLoading :em="true" :colored="false"/>
@@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, inject } from 'vue';
 import { toUnicode } from 'punycode/';
 import * as Misskey from 'misskey-js';
 import MkButton from './MkButton.vue';
@@ -88,9 +88,10 @@ import * as config from '@/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { login } from '@/account.js';
-import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = withDefaults(defineProps<{
 	autoSet?: boolean;
 }>(), {
@@ -127,11 +128,11 @@ const emailAbortController = ref<null | AbortController>(null);
 
 const shouldDisableSubmitting = computed((): boolean => {
 	return submitting.value ||
-		instance.enableHcaptcha && !hCaptchaResponse.value ||
-		instance.enableMcaptcha && !mCaptchaResponse.value ||
-		instance.enableRecaptcha && !reCaptchaResponse.value ||
-		instance.enableTurnstile && !turnstileResponse.value ||
-		instance.emailRequiredForSignup && emailState.value !== 'ok' ||
+		serverMetadata.enableHcaptcha && !hCaptchaResponse.value ||
+		serverMetadata.enableMcaptcha && !mCaptchaResponse.value ||
+		serverMetadata.enableRecaptcha && !reCaptchaResponse.value ||
+		serverMetadata.enableTurnstile && !turnstileResponse.value ||
+		serverMetadata.emailRequiredForSignup && emailState.value !== 'ok' ||
 		usernameState.value !== 'ok' ||
 		passwordRetypeState.value !== 'match';
 });
@@ -260,7 +261,7 @@ async function onSubmit(): Promise<void> {
 			'g-recaptcha-response': reCaptchaResponse.value,
 			'turnstile-response': turnstileResponse.value,
 		});
-		if (instance.emailRequiredForSignup) {
+		if (serverMetadata.emailRequiredForSignup) {
 			os.alert({
 				type: 'success',
 				title: i18n.ts._signup.almostThere,
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
index 9df3ec0c30..fb2cdb4d30 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
+++ b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
@@ -9,7 +9,7 @@ import { StoryObj } from '@storybook/vue3';
 import { onBeforeUnmount } from 'vue';
 import MkSignupServerRules from './MkSignupDialog.rules.vue';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+import { instance } from '@/server-metadata.js';
 export const Empty = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 59a3651cd4..9ab6a5cce3 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<MkSpacer :marginMin="20" :marginMax="28">
 		<div class="_gaps_m">
-			<div v-if="instance.disableRegistration">
+			<div v-if="serverMetadata.disableRegistration">
 				<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
 			</div>
 
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
 
 				<ol class="_gaps_s" :class="$style.rules">
-					<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
+					<li v-for="item in serverMetadata.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
 				</ol>
 
 				<MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch>
@@ -34,8 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ tosPrivacyPolicyLabel }}</template>
 				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
 				<div class="_gaps_s">
-					<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
-					<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
+					<div v-if="availableTos"><a :href="serverMetadata.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
+					<div v-if="availablePrivacyPolicy"><a :href="serverMetadata.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
 				</div>
 
 				<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
@@ -62,8 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref } from 'vue';
-import { instance } from '@/instance.js';
+import { computed, inject, ref } from 'vue';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -71,9 +70,11 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
 
-const availableServerRules = instance.serverRules.length > 0;
-const availableTos = instance.tosUrl != null && instance.tosUrl !== '';
-const availablePrivacyPolicy = instance.privacyPolicyUrl != null && instance.privacyPolicyUrl !== '';
+const serverMetadata = inject('serverMetadata');
+
+const availableServerRules = serverMetadata.serverRules.length > 0;
+const availableTos = serverMetadata.tosUrl != null && serverMetadata.tosUrl !== '';
+const availablePrivacyPolicy = serverMetadata.privacyPolicyUrl != null && serverMetadata.privacyPolicyUrl !== '';
 
 const agreeServerRules = ref(false);
 const agreeTosAndPrivacyPolicy = ref(false);
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 80f3a6709c..634df9878c 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -15,14 +15,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.title">
 			<I18n :src="i18n.ts.aboutX" tag="span">
 				<template #x>
-					{{ instance.name ?? host }}
+					{{ serverMetadata.name ?? host }}
 				</template>
 			</I18n>
 		</div>
 		<div :class="$style.text">
 			<I18n :src="i18n.ts._aboutMisskey.thisIsModifiedVersion" tag="span">
 				<template #name>
-					{{ instance.name ?? host }}
+					{{ serverMetadata.name ?? host }}
 				</template>
 			</I18n>
 			<I18n :src="i18n.ts.correspondingSourceIsAvailable" tag="span">
@@ -40,13 +40,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { host } from '@/config.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { miLocalStorage } from '@/local-storage.js';
 import * as os from '@/os.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index ca87316bf7..0ee3fd18b3 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
+import { computed, watch, onUnmounted, provide, ref, shallowRef, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { BasicTimelineType } from '@/timelines.js';
 import MkNotes from '@/components/MkNotes.vue';
@@ -25,10 +25,11 @@ import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
 import * as sound from '@/scripts/sound.js';
 import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 import { Paging } from '@/components/MkPagination.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = withDefaults(defineProps<{
 	src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
 	list?: string;
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 9adc8d466c..4015c9d69f 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 									<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
 								</template>
 							</I18n>
-							<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
+							<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: serverMetadata.name ?? host }) }}</div>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
 								<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
 								<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
@@ -148,7 +148,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, watch } from 'vue';
+import { inject, ref, shallowRef, watch } from 'vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import XNote from '@/components/MkTutorialDialog.Note.vue';
@@ -157,11 +157,12 @@ import XPostNote from '@/components/MkTutorialDialog.PostNote.vue';
 import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { host } from '@/config.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import * as os from '@/os.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = defineProps<{
 	initialPage?: number;
 }>();
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index f0b9606590..826a6fc3ea 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_panel" :class="$style.root">
-	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
+	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? mediaProxy.getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
 	<MkAvatar :class="$style.avatar" :user="user" indicator/>
 	<div :class="$style.title">
 		<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
@@ -35,15 +35,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
+import { inject } from 'vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { defaultStore } from '@/store.js';
 
+const mediaProxy = inject('mediaProxy');
+
 defineProps<{
 	user: Misskey.entities.UserDetailed;
 }>();
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index 17a9254d01..903e48ed40 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkPagination :pagination="pagination">
 	<template #empty>
 		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.noUsers }}</div>
 		</div>
 	</template>
@@ -21,10 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import MkUserInfo from '@/components/MkUserInfo.vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = withDefaults(defineProps<{
 	pagination: Paging;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index ea1241002e..b5b2df520a 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
 		<div v-if="user != null">
-			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
+			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? mediaProxy.getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
 				<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
 			</div>
 			<svg viewBox="0 0 128 128" :class="$style.avatarBack">
@@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { inject, onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import { userPage } from '@/filters/user.js';
@@ -67,7 +67,9 @@ import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
 import { $i } from '@/account.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+
+const serverMetadata = inject('serverMetadata');
+const mediaProxy = inject('mediaProxy');
 
 const props = defineProps<{
 	showing: boolean;
@@ -88,7 +90,7 @@ const left = ref(0);
 
 function showMenu(ev: MouseEvent) {
 	if (user.value == null) return;
-	const { menu, cleanup } = getUserMenu(user.value);
+	const { menu, cleanup } = getUserMenu({ serverMetadata }, user.value);
 	os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
 }
 
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 514350c930..69f3dc1b37 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div class="_gaps" style="text-align: center;">
 							<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
-							<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
+							<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: serverMetadata.name ?? host }) }}</div>
 							<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
 								<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<div class="_gaps" style="text-align: center;">
 							<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
 							<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
-							<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
+							<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: serverMetadata.name ?? host }) }}</div>
 							<div class="_buttonsCenter" style="margin-top: 16px;">
 								<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
 							</div>
@@ -128,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, shallowRef, watch, nextTick, defineAsyncComponent } from 'vue';
+import { ref, shallowRef, watch, nextTick, defineAsyncComponent, inject } from 'vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
@@ -136,19 +136,19 @@ import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
 import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { host } from '@/config.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-
-// eslint-disable-next-line vue/no-setup-props-reactivity-loss
+ 
 const page = ref(defaultStore.state.accountSetupWizard);
 
 watch(page, () => {
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 445780eca7..2655acc848 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -6,19 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div v-if="instance" :class="$style.root">
 	<div :class="[$style.main, $style.panel]">
-		<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/>
+		<img :src="serverMetadata.iconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/>
 		<button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
 		<div :class="$style.mainFg">
 			<h1 :class="$style.mainTitle">
 				<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
-				<!-- <img class="logo" v-if="instance.logoImageUrl" :src="instance.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
+				<!-- <img class="logo" v-if="serverMetadata.logoImageUrl" :src="serverMetadata.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
 				<span>{{ instanceName }}</span>
 			</h1>
 			<div :class="$style.mainAbout">
 				<!-- eslint-disable-next-line vue/no-v-html -->
-				<div v-html="instance.description || i18n.ts.headlineMisskey"></div>
+				<div v-html="serverMetadata.description || i18n.ts.headlineMisskey"></div>
 			</div>
-			<div v-if="instance.disableRegistration" :class="$style.mainWarn">
+			<div v-if="serverMetadata.disableRegistration" :class="$style.mainWarn">
 				<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
 			</div>
 			<div class="_gaps_s" :class="$style.mainActions">
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div :class="$style.statsItemCount"><MkNumber :value="stats.originalNotesCount"/></div>
 		</div>
 	</div>
-	<div v-if="instance.policies.ltlAvailable" :class="[$style.tl, $style.panel]">
+	<div v-if="serverMetadata.policies.ltlAvailable" :class="[$style.tl, $style.panel]">
 		<div :class="$style.tlHeader">{{ i18n.ts.letsLookAtTimeline }}</div>
 		<div :class="$style.tlBody">
 			<MkTimeline src="local"/>
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XSigninDialog from '@/components/MkSigninDialog.vue';
 import XSignupDialog from '@/components/MkSignupDialog.vue';
@@ -62,11 +62,11 @@ import { instanceName } from '@/config.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 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.js';
-import type { MenuItem } from '@/types/menu.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const stats = ref<Misskey.entities.StatsResponse | null>(null);
 
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index bdaa8a809f..c6a0c4a320 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -42,16 +42,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, inject } from 'vue';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { url as local, host } from '@/config.js';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
 
-type Ad = (typeof instance)['ads'][number];
+const serverMetadata = inject('serverMetadata');
+
+type Ad = (typeof serverMetadata)['ads'][number];
 
 const props = defineProps<{
 	prefer: string[];
@@ -68,7 +69,7 @@ const choseAd = (): Ad | null => {
 		return props.specify;
 	}
 
-	const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
+	const allAds = serverMetadata.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
 		...ad,
 		ratio: 0,
 	} : ad);
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 35c07bc80c..018cb68280 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -40,16 +40,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, ref, computed } from 'vue';
+import { watch, ref, computed, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
 import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
 import MkA from './MkA.vue';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { acct, userPage } from '@/filters/user.js';
 import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
 import { defaultStore } from '@/store.js';
 
+const mediaProxy = inject('mediaProxy');
+
 const animation = ref(defaultStore.state.animation);
 const squareAvatars = ref(defaultStore.state.squareAvatars);
 const useBlurEffect = ref(defaultStore.state.useBlurEffect);
@@ -83,7 +84,7 @@ const bound = computed(() => props.link
 
 const url = computed(() => {
 	if (props.user.avatarUrl == null) return null;
-	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
+	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return mediaProxy.getStaticImageUrl(props.user.avatarUrl);
 	return props.user.avatarUrl;
 });
 
@@ -93,7 +94,7 @@ function onClick(ev: MouseEvent): void {
 }
 
 function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
-	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url);
+	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return mediaProxy.getStaticImageUrl(decoration.url);
 	return decoration.url;
 }
 
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index dff56cd7f0..82e999ddf9 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -26,7 +26,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, inject, ref } from 'vue';
-import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { defaultStore } from '@/store.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import * as os from '@/os.js';
@@ -36,6 +35,8 @@ import * as sound from '@/scripts/sound.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
 
+const mediaProxy = inject('mediaProxy');
+
 const props = defineProps<{
 	name: string;
 	normal?: boolean;
@@ -69,14 +70,14 @@ const url = computed(() => {
 	const proxied =
 		(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
 			? rawUrl.value
-			: getProxiedImageUrl(
+			: mediaProxy.getProxiedImageUrl(
 				rawUrl.value,
 				props.useOriginalSize ? undefined : 'emoji',
 				false,
 				true,
 			);
 	return defaultStore.reactiveState.disableShowingAnimatedImages.value
-		? getStaticImageUrl(proxied)
+		? mediaProxy.getStaticImageUrl(proxied)
 		: proxied;
 });
 
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index c594cc752b..6404bea38e 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
 	<div :class="$style.root">
-		<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+		<img v-if="serverMetadata.serverErrorImageUrl" :class="$style.img" :src="serverMetadata.serverErrorImageUrl" class="_ghost"/>
 		<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
 		<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
 	</div>
@@ -14,10 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
-import { serverErrorImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const emit = defineEmits<{
 	(ev: 'retry'): void;
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 8f4e3b853a..a476742553 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -25,14 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, inject, ref } from 'vue';
 import { toUnicode as decodePunycode } from 'punycode/';
 import { url as local } from '@/config.js';
 import * as os from '@/os.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
-import { isEnabledUrlPreview } from '@/instance.js';
 import { MkABehavior } from '@/components/global/MkA.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 function safeURIDecode(str: string): string {
 	try {
 		return decodeURIComponent(str);
@@ -55,7 +56,7 @@ const url = new URL(props.url);
 if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url');
 const el = ref();
 
-if (props.showUrlPreview && isEnabledUrlPreview.value) {
+if (props.showUrlPreview && serverMetadata.enableUrlPreview) {
 	useTooltip(el, (showing) => {
 		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
 			showing,
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index e0c7956f6e..edbdba8df9 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -6,18 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div class="_gaps" :class="$style.textRoot">
 	<Mfm :text="block.text ?? ''" :isNote="false"/>
-	<div v-if="isEnabledUrlPreview" class="_gaps_s">
+	<div v-if="serverMetadata.enableUrlPreview" class="_gaps_s">
 		<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
 	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent, inject } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
-import { isEnabledUrlPreview } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
 
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index e135bc69a0..7f56f673c4 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -106,10 +106,6 @@ export const ROLE_POLICIES = [
 export const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP';
 export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
 
-export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg';
-export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
-export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
-
 export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
 export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
 	tada: ['speed=', 'delay='],
@@ -127,7 +123,7 @@ export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
 	position: ['x=', 'y='],
 	fg: ['color='],
 	bg: ['color='],
-  border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
+	border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
 	font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
 	blur: [],
 	rainbow: ['speed=', 'delay='],
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 59ca455362..195629e2af 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -4,7 +4,6 @@
  */
 
 export type Keys =
-	'v' |
 	'lastVersion' |
 	'instance' |
 	'instanceCachedAt' |
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index c04f399c6d..e156d103ab 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkLoading v-if="!loaded"/>
 <Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
 	<div v-show="loaded" :class="$style.root">
-		<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
+		<img v-if="serverMetadata.serverErrorImageUrl" :src="serverMetadata.serverErrorImageUrl" class="_ghost" :class="$style.img"/>
 		<div class="_gaps">
 			<div><b><i class="ti ti-alert-triangle"></i> {{ i18n.ts.pageLoadError }}</b></div>
 			<div v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</div>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkLink from '@/components/MkLink.vue';
@@ -36,7 +36,8 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { defaultStore } from '@/store.js';
-import { serverErrorImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = withDefaults(defineProps<{
 	error?: Error;
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index a16c1eeacc..f855e77f34 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -46,21 +46,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</FormLink>
 					</div>
 				</FormSection>
-				<FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
+				<FormSection v-if="serverMetadata.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
 					<div class="_gaps_s">
 						<MkInfo>
-							{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
+							{{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: serverMetadata.name }) }}
 						</MkInfo>
-						<FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
+						<FormLink v-if="serverMetadata.repositoryUrl" :to="serverMetadata.repositoryUrl" external>
 							<template #icon><i class="ti ti-code"></i></template>
 							{{ i18n.ts._aboutMisskey.source }}
 						</FormLink>
-						<FormLink v-if="instance.providesTarball" :to="`/tarball/misskey-${version}.tar.gz`" external>
+						<FormLink v-if="serverMetadata.providesTarball" :to="`/tarball/misskey-${version}.tar.gz`" external>
 							<template #icon><i class="ti ti-download"></i></template>
 							{{ i18n.ts._aboutMisskey.source }}
 							<template #suffix>Tarball</template>
 						</FormLink>
-						<MkInfo v-if="!instance.repositoryUrl && !instance.providesTarball" warn>
+						<MkInfo v-if="!serverMetadata.repositoryUrl && !serverMetadata.providesTarball" warn>
 							{{ i18n.ts.sourceCodeIsNotYetProvided }}
 						</MkInfo>
 					</div>
@@ -131,7 +131,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { nextTick, onBeforeUnmount, ref, shallowRef, computed } from 'vue';
+import { nextTick, onBeforeUnmount, ref, shallowRef, computed, inject } from 'vue';
 import { version } from '@/config.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
@@ -139,13 +139,14 @@ import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { physics } from '@/scripts/physics.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
 import { $i } from '@/account.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const patronsWithIcon = [{
 	name: 'カイヤン',
 	icon: 'https://assets.misskey-hub.net/patrons/a2820716883e408cb87773e377ce7c8d.jpg',
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
index 84419b3bef..0e05a9d808 100644
--- a/packages/frontend/src/pages/about.overview.vue
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -5,18 +5,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_gaps_m">
-	<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
+	<div :class="$style.banner" :style="{ backgroundImage: `url(${ serverMetadata.bannerUrl })` }">
 		<div style="overflow: clip;">
-			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
+			<img :src="serverMetadata.iconUrl ?? serverMetadata.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
 			<div :class="$style.bannerName">
-				<b>{{ instance.name ?? host }}</b>
+				<b>{{ serverMetadata.name ?? host }}</b>
 			</div>
 		</div>
 	</div>
 
 	<MkKeyValue>
 		<template #key>{{ i18n.ts.description }}</template>
-		<template #value><div v-html="instance.description"></div></template>
+		<template #value><div v-html="serverMetadata.description"></div></template>
 	</MkKeyValue>
 
 	<FormSection>
@@ -25,13 +25,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #key>Misskey</template>
 				<template #value>{{ version }}</template>
 			</MkKeyValue>
-			<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
+			<div v-html="i18n.tsx.poweredByMisskeyDescription({ name: serverMetadata.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>
+			<FormLink v-if="serverMetadata.repositoryUrl || serverMetadata.providesTarball" :to="serverMetadata.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
 				<template #icon><i class="ti ti-code"></i></template>
 				{{ i18n.ts.sourceCode }}
 			</FormLink>
@@ -44,51 +44,51 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<FormSection>
 		<div class="_gaps_m">
 			<FormSplit>
-				<MkKeyValue :copy="instance.maintainerName">
+				<MkKeyValue :copy="serverMetadata.maintainerName">
 					<template #key>{{ i18n.ts.administrator }}</template>
 					<template #value>
-						<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
+						<template v-if="serverMetadata.maintainerName">{{ serverMetadata.maintainerName }}</template>
 						<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 					</template>
 				</MkKeyValue>
-				<MkKeyValue :copy="instance.maintainerEmail">
+				<MkKeyValue :copy="serverMetadata.maintainerEmail">
 					<template #key>{{ i18n.ts.contact }}</template>
 					<template #value>
-						<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
+						<template v-if="serverMetadata.maintainerEmail">{{ serverMetadata.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>
+						<MkLink v-if="serverMetadata.inquiryUrl" :url="serverMetadata.inquiryUrl" target="_blank">{{ serverMetadata.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>
+				<FormLink v-if="serverMetadata.impressumUrl" :to="serverMetadata.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">
+				<MkFolder v-if="serverMetadata.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">
+						<li v-for="item in serverMetadata.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>
+				<FormLink v-if="serverMetadata.tosUrl" :to="serverMetadata.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>
+				<FormLink v-if="serverMetadata.privacyPolicyUrl" :to="serverMetadata.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>
+				<FormLink v-if="serverMetadata.feedbackUrl" :to="serverMetadata.feedbackUrl" external>
 					<template #icon><i class="ti ti-message"></i></template>
 					<template #default>{{ i18n.ts.feedback }}</template>
 				</FormLink>
@@ -126,9 +126,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 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';
@@ -139,6 +139,8 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkLink from '@/components/MkLink.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 const initStats = () => misskeyApi('stats', {});
 </script>
 
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 73c5e1919f..c477039735 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -92,7 +92,7 @@ import FormSuspense from '@/components/form/suspense.vue';
 import FormSlot from '@/components/form/slot.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 
 const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
@@ -142,7 +142,7 @@ function save() {
 		turnstileSiteKey: turnstileSiteKey.value,
 		turnstileSecretKey: turnstileSecretKey.value,
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 </script>
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index fe1b7c561d..ed614781ca 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -112,7 +112,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { instance, fetchInstance } from '@/instance.js';
+import { instance, fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
@@ -169,7 +169,7 @@ function save() {
 		feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
 		manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 4a858887f3..bb72dfa72d 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -74,7 +74,7 @@ import FormSplit from '@/components/form/split.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance, instance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
@@ -99,10 +99,11 @@ async function init() {
 }
 
 async function testEmail() {
+	const serverMetadata = await fetchServerMetadata();
 	const { canceled, result: destination } = await os.inputText({
 		title: i18n.ts.destination,
 		type: 'email',
-		default: instance.maintainerEmail ?? '',
+		default: serverMetadata.maintainerEmail ?? '',
 		placeholder: 'test@example.com',
 		minLength: 1,
 	});
@@ -124,7 +125,7 @@ function save() {
 		smtpUser: smtpUser.value,
 		smtpPass: smtpPass.value,
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index e0b82eb02e..2842c63b18 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -43,7 +43,7 @@ import FormSuspense from '@/components/form/suspense.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
@@ -61,7 +61,7 @@ function save() {
 		deeplAuthKey: deeplAuthKey.value,
 		deeplIsPro: deeplIsPro.value,
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 40dec55deb..0c8a2439cf 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSpacer :contentMax="700" :marginMin="16">
 			<div class="lxpfedzu _gaps">
 				<div class="banner">
-					<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
+					<img :src="serverMetadata.iconUrl || '/favicon.ico'" alt="" class="icon"/>
 				</div>
 
 				<div class="_gaps_s">
@@ -31,11 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
+import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed, inject } from 'vue';
 import { i18n } from '@/i18n.js';
 import MkSuperMenu from '@/components/MkSuperMenu.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { instance } from '@/instance.js';
 import { lookup } from '@/scripts/lookup.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -43,6 +42,8 @@ import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-looku
 import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import { useRouter } from '@/router/supplier.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const isEmpty = (x: string | null) => x == null || x === '';
 
 const router = useRouter();
@@ -61,10 +62,10 @@ const narrow = ref(false);
 const view = ref(null);
 const el = ref<HTMLDivElement | null>(null);
 const pageProps = ref({});
-const noMaintainerInformation = computed(() => isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail));
-const noBotProtection = computed(() => !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile && !instance.enableMcaptcha);
-const noEmailServer = computed(() => !instance.enableEmail);
-const noInquiryUrl = computed(() => isEmpty(instance.inquiryUrl));
+const noMaintainerInformation = computed(() => isEmpty(serverMetadata.maintainerName) || isEmpty(serverMetadata.maintainerEmail));
+const noBotProtection = computed(() => !serverMetadata.disableRegistration && !serverMetadata.enableHcaptcha && !serverMetadata.enableRecaptcha && !serverMetadata.enableTurnstile && !serverMetadata.enableMcaptcha);
+const noEmailServer = computed(() => !serverMetadata.enableEmail);
+const noInquiryUrl = computed(() => isEmpty(serverMetadata.inquiryUrl));
 const thereIsUnresolvedAbuseReport = ref(false);
 const currentPage = computed(() => router.currentRef.value.child);
 
@@ -88,7 +89,7 @@ const menuDef = computed(() => [{
 		icon: 'ti ti-search',
 		text: i18n.ts.lookup,
 		action: adminLookup,
-	}, ...(instance.disableRegistration ? [{
+	}, ...(serverMetadata.disableRegistration ? [{
 		type: 'button',
 		icon: 'ti ti-user-plus',
 		text: i18n.ts.createInviteCode,
diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue
index e090616b26..3fb8a7dc4c 100644
--- a/packages/frontend/src/pages/admin/instance-block.vue
+++ b/packages/frontend/src/pages/admin/instance-block.vue
@@ -38,7 +38,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
@@ -61,7 +61,7 @@ function save() {
 		mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [],
 
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index a75799696d..1702de2a9e 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -78,7 +78,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
@@ -119,7 +119,7 @@ function save() {
 		hiddenTags: hiddenTags.value.split('\n'),
 		preservedUsernames: preservedUsernames.value.split('\n'),
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 5fddb715cd..931ca449cd 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -91,7 +91,7 @@ import FormSuspense from '@/components/form/suspense.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
@@ -143,7 +143,7 @@ function save() {
 		objectStorageSetPublicRead: objectStorageSetPublicRead.value,
 		objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue
index 345cf333b5..807bb20c71 100644
--- a/packages/frontend/src/pages/admin/other-settings.vue
+++ b/packages/frontend/src/pages/admin/other-settings.vue
@@ -48,7 +48,7 @@ import XHeader from './_header_.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkSwitch from '@/components/MkSwitch.vue';
@@ -73,7 +73,7 @@ function save() {
 		enableChartsForRemoteUser: enableChartsForRemoteUser.value,
 		enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
index 81db9f1da9..aaf86ac4a7 100644
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ b/packages/frontend/src/pages/admin/proxy-account.vue
@@ -29,7 +29,7 @@ import MkInfo from '@/components/MkInfo.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
@@ -56,7 +56,7 @@ function save() {
 	os.apiWithDialog('admin/update-meta', {
 		proxyAccountId: proxyAccountId.value,
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 3e948abdf1..6fe42f0611 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -596,7 +596,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, ref, computed } from 'vue';
+import { watch, ref, computed, inject } from 'vue';
 import { throttle } from 'throttle-debounce';
 import RolesEditorFormula from './RolesEditorFormula.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -609,9 +609,10 @@ import MkRange from '@/components/MkRange.vue';
 import FormSlot from '@/components/form/slot.vue';
 import { i18n } from '@/i18n.js';
 import { ROLE_POLICIES } from '@/const.js';
-import { instance } from '@/instance.js';
 import { deepClone } from '@/scripts/clone.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const emit = defineEmits<{
 	(ev: 'update:modelValue', v: any): void;
 }>();
@@ -629,7 +630,7 @@ for (const ROLE_POLICY of ROLE_POLICIES) {
 		role.value.policies[ROLE_POLICY] = {
 			useDefault: true,
 			priority: 0,
-			value: instance.policies[ROLE_POLICY],
+			value: serverMetadata.policies[ROLE_POLICY],
 		};
 	}
 }
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 8b3c906d8a..8dd9bcec8f 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkPagination :pagination="usersPagination">
 							<template #empty>
 								<div class="_fullinfo">
-									<img :src="infoImageUrl" class="_ghost"/>
+									<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 									<div>{{ i18n.ts.noUsers }}</div>
 								</div>
 							</template>
@@ -74,7 +74,8 @@ 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 { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import { useRouter } from '@/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 6fb950494b..e48cabe97b 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -251,7 +251,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 { instance, fetchInstance } from '@/instance.js';
+import { instance, fetchServerMetadata } from '@/server-metadata.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { ROLE_POLICIES } from '@/const.js';
 import { useRouter } from '@/router/supplier.js';
@@ -275,7 +275,7 @@ async function updateBaseRole() {
 	await os.apiWithDialog('admin/roles/update-default-policies', {
 		policies,
 	});
-	fetchInstance(true);
+	fetchServerMetadata(true);
 }
 
 function create() {
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 9bccee89a5..b06ff05b3a 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -138,7 +138,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 
@@ -205,7 +205,7 @@ function save() {
 		truemailAuthKey: truemailAuthKey.value,
 		bannedEmailDomains: bannedEmailDomains.value.split('\n'),
 	}).then(() => {
-		fetchInstance(true);
+		fetchServerMetadata(true);
 	});
 }
 
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index ff9b8d6299..e92b811122 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -41,24 +41,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, ref, computed } from 'vue';
+import { defineAsyncComponent, ref, computed, inject } from 'vue';
 import XHeader from './_header_.vue';
 import * as os from '@/os.js';
-import { fetchInstance, instance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
-const serverRules = ref<string[]>(instance.serverRules);
+const serverRules = ref<string[]>(serverMetadata.serverRules);
 
 const save = async () => {
 	await os.apiWithDialog('admin/update-meta', {
 		serverRules: serverRules.value,
 	});
-	fetchInstance(true);
+	fetchServerMetadata(true);
 };
 
 const remove = (index: number): void => {
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 6f45c212ec..cbff964bb4 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
 					</MkInput>
 
-					<MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn>
+					<MkInfo v-if="!serverMetadata.providesTarball && !repositoryUrl" warn>
 						{{ i18n.ts.repositoryUrlOrTarballRequired }}
 					</MkInfo>
 
@@ -205,7 +205,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from 'vue';
+import { ref, computed, inject } from 'vue';
 import XHeader from './_header_.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -216,13 +216,15 @@ import FormSplit from '@/components/form/split.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { fetchInstance, instance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSelect from '@/components/MkSelect.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 const name = ref<string | null>(null);
 const shortName = ref<string | null>(null);
 const description = ref<string | null>(null);
@@ -310,7 +312,7 @@ async function save() {
 		urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value,
 	});
 
-	fetchInstance(true);
+	fetchServerMetadata(true);
 }
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue
index b31807f9f5..8c46ca57b7 100644
--- a/packages/frontend/src/pages/ads.vue
+++ b/packages/frontend/src/pages/ads.vue
@@ -9,16 +9,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkSpacer :contentMax="500">
 		<div class="_gaps">
-			<MkAd v-for="ad in instance.ads" :key="ad.id" :specify="ad"/>
+			<MkAd v-for="ad in serverMetadata.ads" :key="ad.id" :specify="ad"/>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 definePageMetadata(() => ({
 	title: i18n.ts.ads,
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
index 1f2bee5a77..64d6f2fb6b 100644
--- a/packages/frontend/src/pages/contact.vue
+++ b/packages/frontend/src/pages/contact.vue
@@ -8,24 +8,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader/></template>
 	<MkSpacer :contentMax="600" :marginMin="20">
 		<div class="_gaps_m">
-			<MkKeyValue :copy="instance.maintainerName">
+			<MkKeyValue :copy="serverMetadata.maintainerName">
 				<template #key>{{ i18n.ts.administrator }}</template>
 				<template #value>
-					<template v-if="instance.maintainerName">{{ instance.maintainerName }}</template>
+					<template v-if="serverMetadata.maintainerName">{{ serverMetadata.maintainerName }}</template>
 					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
-			<MkKeyValue :copy="instance.maintainerEmail">
+			<MkKeyValue :copy="serverMetadata.maintainerEmail">
 				<template #key>{{ i18n.ts.contact }}</template>
 				<template #value>
-					<template v-if="instance.maintainerEmail">{{ instance.maintainerEmail }}</template>
+					<template v-if="serverMetadata.maintainerEmail">{{ serverMetadata.maintainerEmail }}</template>
 					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
-			<MkKeyValue :copy="instance.inquiryUrl">
+			<MkKeyValue :copy="serverMetadata.inquiryUrl">
 				<template #key>{{ i18n.ts.inquiry }}</template>
 				<template #value>
-					<MkLink v-if="instance.inquiryUrl" :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
+					<MkLink v-if="serverMetadata.inquiryUrl" :url="serverMetadata.inquiryUrl" target="_blank">{{ serverMetadata.inquiryUrl }}</MkLink>
 					<span v-else style="opacity: 0.7;">({{ i18n.ts.none }})</span>
 				</template>
 			</MkKeyValue>
@@ -35,12 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import { i18n } from '@/i18n.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';
 
+const serverMetadata = inject('serverMetadata');
+
 definePageMetadata(() => ({
 	title: i18n.ts.inquiry,
 	icon: 'ti ti-help-circle',
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 3026d00a2c..579895e2bb 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<div v-else class="_fullinfo">
-		<img :src="infoImageUrl" class="_ghost"/>
+		<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 		<div>{{ i18n.ts.nothing }}</div>
 	</div>
 </div>
@@ -82,7 +82,8 @@ import MkInfo from '@/components/MkInfo.vue';
 import MkMediaList from '@/components/MkMediaList.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import bytes from '@/filters/bytes.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -99,12 +100,12 @@ const file = ref<Misskey.entities.DriveFile>();
 const folderHierarchy = computed(() => {
 	if (!file.value) return [i18n.ts.drive];
 	const folderNames = [i18n.ts.drive];
-	
+
 	function get(folder: Misskey.entities.DriveFolder) {
 		if (folder.parent) get(folder.parent);
 		folderNames.push(folder.name);
 	}
-	
+
 	if (file.value.folder) get(file.value.folder);
 	return folderNames;
 });
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index c3d4cae4aa..1bfbe6a5e9 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="pagination">
 			<template #empty>
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.noNotes }}</div>
 				</div>
 			</template>
@@ -31,7 +31,8 @@ import MkNote from '@/components/MkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 
 const pagination = {
 	endpoint: 'i/favorites' as const,
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 8991af8086..9a7cc27bb2 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination ref="paginationComponent" :pagination="pagination">
 			<template #empty>
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.noFollowRequests }}</div>
 				</div>
 			</template>
@@ -44,7 +44,8 @@ import { userPage, acct } from '@/filters/user.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 
 const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 4ba428d536..7fbb1b66c0 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -132,7 +132,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, watch } from 'vue';
+import { ref, computed, watch, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkChart from '@/components/MkChart.vue';
 import MkObjectView from '@/components/MkObjectView.vue';
@@ -152,10 +152,11 @@ import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 import { dateString } from '@/filters/date.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 
+const mediaProxy = inject('mediaProxy');
+
 const props = defineProps<{
 	host: string;
 }>();
@@ -198,7 +199,7 @@ async function fetch(): Promise<void> {
 	isBlocked.value = instance.value?.isBlocked ?? false;
 	isSilenced.value = instance.value?.isSilenced ?? false;
 	isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
-	faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
+	faviconUrl.value = mediaProxy.getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? mediaProxy.getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
 	moderationNote.value = instance.value?.moderationNote ?? '';
 }
 
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 25e56d2b8d..d9672be1a1 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>
 		<MkPageHeader/>
 	</template>
-	<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
+	<MKSpacer v-if="!serverMetadata.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
 		<div :class="$style.root">
-			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.serverErrorImageUrl" :class="$style.img" :src="serverMetadata.serverErrorImageUrl" class="_ghost"/>
 			<div :class="$style.text">
 				<i class="ti ti-alert-triangle"></i>
 				{{ i18n.ts.nothing }}
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, shallowRef } from 'vue';
+import { computed, inject, ref, shallowRef } from 'vue';
 import type * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
@@ -45,13 +45,14 @@ import MkButton from '@/components/MkButton.vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { serverErrorImageUrl, instance } from '@/instance.js';
 import { $i } from '@/account.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 const currentInviteLimit = ref<null | number>(null);
-const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
-const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
+const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && serverMetadata.policies.inviteLimit))) as number;
+const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && serverMetadata.policies.inviteLimitCycle)) as number;
 
 const pagination: Paging = {
 	endpoint: 'invite/list' as const,
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 954246ff93..17c178e26e 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MKSpacer v-if="!(typeof error === 'undefined')" :contentMax="1200">
 		<div :class="$style.root">
-			<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.serverErrorImageUrl" :class="$style.img" :src="serverMetadata.serverErrorImageUrl" class="_ghost"/>
 			<p :class="$style.text">
 				<i class="ti ti-alert-triangle"></i>
 				{{ i18n.ts.nothing }}
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { watch, computed, ref } from 'vue';
+import { watch, computed, ref, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -43,7 +43,8 @@ import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkButton from '@/components/MkButton.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { serverErrorImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = defineProps<{
 	listId: string;
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 21c96348f0..134b369c84 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div>
 			<div v-if="antennas.length === 0" class="empty">
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.nothing }}</div>
 				</div>
 			</div>
@@ -33,7 +33,8 @@ import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache } from '@/cache.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 
 const antennas = computed(() => antennasCache.value.value ?? []);
 
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 82fde284c1..40456305e5 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div class="_gaps">
 			<div v-if="items.length === 0" class="empty">
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.nothing }}</div>
 				</div>
 			</div>
@@ -36,7 +36,8 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { userListsCache } from '@/cache.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import { signinRequired } from '@/account.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 93a792c42f..5497908fe5 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -6,18 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div class="_fullinfo">
-		<img :src="notFoundImageUrl" class="_ghost"/>
+		<img v-if="serverMetadata.notFoundImageUrl" :src="serverMetadata.notFoundImageUrl" class="_ghost"/>
 		<div>{{ i18n.ts.notFoundDescription }}</div>
 	</div>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, inject } from 'vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { pleaseLogin } from '@/scripts/please-login.js';
-import { notFoundImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = defineProps<{
 	showLoginPopup?: boolean;
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index cb1ce9b918..90cf270e43 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<img
 								v-else-if="instance.backgroundImageUrl || instance.bannerUrl"
 								:class="[$style.pageBannerBg, $style.pageBannerBgFallback1]"
-								:src="getStaticImageUrl(instance.backgroundImageUrl ?? instance.bannerUrl!)"
+								:src="mediaProxy.getStaticImageUrl(instance.backgroundImageUrl ?? instance.bannerUrl!)"
 							/>
 							<div v-else :class="[$style.pageBannerBg, $style.pageBannerBgFallback2]"></div>
 						</div>
@@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref, defineAsyncComponent } from 'vue';
+import { computed, watch, ref, defineAsyncComponent, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import XPage from '@/components/page/page.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -117,12 +117,13 @@ import { pageViewInterruptors, defaultStore } from '@/store.js';
 import { deepClone } from '@/scripts/clone.js';
 import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
-import { instance } from '@/instance.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
 import { MenuItem } from '@/types/menu';
 
+const serverMetadata = inject('serverMetadata');
+const mediaProxy = inject('mediaProxy');
+
 const router = useRouter();
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index ce80579cf9..62d7757d55 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="role">{{ role.description }}</div>
 			<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/>
 			<div v-else-if="!visible" class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 				<div>{{ i18n.ts.nothing }}</div>
 			</div>
 		</div>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
 		<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/>
 		<div v-else-if="!visible" class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 	</MkSpacer>
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, ref } from 'vue';
+import { computed, watch, ref, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkUserList from '@/components/MkUserList.vue';
@@ -44,7 +44,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { instanceName } from '@/config.js';
-import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = withDefaults(defineProps<{
 	role: string;
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 9cf7fbe8d8..79032cf7b4 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, toRef, watch } from 'vue';
+import { computed, inject, ref, toRef, watch } from 'vue';
 import type { UserDetailed } from 'misskey-js/entities.js';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkNotes from '@/components/MkNotes.vue';
@@ -66,7 +66,8 @@ import { useRouter } from '@/router/supplier.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const props = withDefaults(defineProps<{
 	query?: string;
@@ -87,7 +88,7 @@ const notePagination = ref<Paging>();
 const user = ref<UserDetailed | null>(null);
 const hostInput = ref(toRef(props, 'host').value);
 
-const noteSearchableScope = instance.noteSearchableScope ?? 'local';
+const noteSearchableScope = serverMetadata.noteSearchableScope ?? 'local';
 
 const hostSelect = ref<'all' | 'local' | 'specified'>('all');
 
@@ -121,7 +122,7 @@ if (props.userId != null) {
 }
 
 function selectUser() {
-	os.selectUser({ includeSelf: true, localOnly: instance.noteSearchableScope === 'local' }).then(_user => {
+	os.selectUser({ includeSelf: true, localOnly: serverMetadata.noteSearchableScope === 'local' }).then(_user => {
 		user.value = _user;
 		hostInput.value = _user.host ?? '';
 	});
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index 38d7548fa8..ce34cc16d8 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
 		<MkSpacer v-if="tab === 'note'" key="note" :contentMax="800">
-			<div v-if="notesSearchAvailable || ignoreNotesSearchAvailable">
+			<div v-if="isNotesSearchAvailable(serverMetadata) || ignoreNotesSearchAvailable">
 				<XNote v-bind="props"/>
 			</div>
 			<div v-else>
@@ -25,13 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, toRef } from 'vue';
+import { computed, defineAsyncComponent, inject, ref, toRef } from 'vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { notesSearchAvailable } from '@/scripts/check-permissions.js';
+import { isNotesSearchAvailable } from '@/scripts/check-permissions.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = withDefaults(defineProps<{
 	query?: string,
 	userId?: string,
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 0e0c1f4c0c..47d02a56fb 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<FormPagination ref="list" :pagination="pagination">
 		<template #empty>
 			<div class="_fullinfo">
-				<img :src="infoImageUrl" class="_ghost"/>
+				<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 				<div>{{ i18n.ts.nothing }}</div>
 			</div>
 		</template>
@@ -52,7 +52,8 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 
 const list = ref<InstanceType<typeof FormPagination>>();
 
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index f226647569..38badf05d7 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="instance.enableEmail" class="_gaps_m">
+<div v-if="serverMetadata.enableEmail" class="_gaps_m">
 	<FormSection first>
 		<template #label>{{ i18n.ts.emailAddress }}</template>
 		<MkInput v-model="emailAddress" type="email" manualSave>
@@ -42,13 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</FormSection>
 </div>
-<div v-if="!instance.enableEmail" class="_gaps_m">
+<div v-if="!serverMetadata.enableEmail" class="_gaps_m">
 	<MkInfo>{{ i18n.ts.emailNotSupported }}</MkInfo>
 </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref, watch, computed } from 'vue';
+import { onMounted, ref, watch, computed, inject } from 'vue';
 import FormSection from '@/components/form/section.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -58,7 +58,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { signinRequired } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 7d16740a3e..5e2e7b03e0 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -28,17 +28,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { computed, inject, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
 import { i18n } from '@/i18n.js';
 import MkInfo from '@/components/MkInfo.vue';
 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 { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
 import * as os from '@/os.js';
 import { useRouter } from '@/router/supplier.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const indexInfo = {
 	title: i18n.ts.settings,
 	icon: 'ti ti-settings',
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index f4ee7dffbf..ac6d88cb44 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="renoteMutingPagination">
 			<template #empty>
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.noUsers }}</div>
 				</div>
 			</template>
@@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="mutingPagination">
 			<template #empty>
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.noUsers }}</div>
 				</div>
 			</template>
@@ -97,7 +97,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="blockingPagination">
 			<template #empty>
 				<div class="_fullinfo">
-					<img :src="infoImageUrl" class="_ghost"/>
+					<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 					<div>{{ i18n.ts.noUsers }}</div>
 				</div>
 			</template>
@@ -136,7 +136,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import { signinRequired } from '@/account.js';
 import MkFolder from '@/components/MkFolder.vue';
 
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 7d192bcbea..472be73133 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, onActivated, ref, watch } from 'vue';
+import { computed, inject, onActivated, ref, watch } from 'vue';
 import JSON5 from 'json5';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
@@ -83,7 +83,6 @@ import { selectFile } from '@/scripts/select-file.js';
 import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
 import { ColdDeviceStorage, defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import { uniqueBy } from '@/scripts/array.js';
 import { fetchThemes, getThemes } from '@/theme-store.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -91,6 +90,8 @@ import { miLocalStorage } from '@/local-storage.js';
 import { unisonReload } from '@/scripts/unison-reload.js';
 import * as os from '@/os.js';
 
+const serverMetadata = inject('serverMetadata');
+
 async function reloadAsk() {
 	const { canceled } = await os.confirm({
 		type: 'info',
@@ -104,10 +105,10 @@ async function reloadAsk() {
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
 
-const instanceDarkTheme = computed(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
+const instanceDarkTheme = computed(() => serverMetadata.defaultDarkTheme ? JSON5.parse(serverMetadata.defaultDarkTheme) : null);
 const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
 const builtinDarkThemes = computed(() => builtinThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
-const instanceLightTheme = computed(() => instance.defaultLightTheme ? JSON5.parse(instance.defaultLightTheme) : null);
+const instanceLightTheme = computed(() => serverMetadata.defaultLightTheme ? JSON5.parse(serverMetadata.defaultLightTheme) : null);
 const installedLightThemes = computed(() => installedThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
 const builtinLightThemes = computed(() => builtinThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
 const themes = computed(() => uniqueBy([instanceDarkTheme.value, instanceLightTheme.value, ...builtinThemes.value, ...installedThemes.value].filter(x => x != null), theme => theme.id));
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index cc1ed3d01f..3a88264f8b 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -34,13 +34,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, watch, provide, shallowRef, ref, onMounted, onActivated } from 'vue';
+import { computed, watch, provide, shallowRef, ref, onMounted, onActivated, inject } from 'vue';
+import { scroll } from '@@/js/scroll.js';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
+import type { BasicTimelineType } from '@/timelines.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { scroll } from '@@/js/scroll.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
@@ -53,17 +54,18 @@ import { deepMerge } from '@/scripts/merge.js';
 import { MenuItem } from '@/types/menu.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
-import type { BasicTimelineType } from '@/timelines.js';
 
 provide('shouldOmitHeaderTitle', true);
 
+const serverMetadata = inject('serverMetadata');
+
 const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
 const rootEl = shallowRef<HTMLElement>();
 
 type TimelinePageSrc = BasicTimelineType | `list:${string}`;
 
 const queue = ref(0);
-const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
+const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline(serverMetadata, 'local') ? 'local' : 'global');
 const src = computed<TimelinePageSrc>({
 	get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
 	set: (x) => saveSrc(x),
@@ -240,8 +242,8 @@ function closeTutorial(): void {
 }
 
 function switchTlIfNeeded() {
-	if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(src.value)) {
-		src.value = availableBasicTimelines()[0];
+	if (isBasicTimeline(src.value) && !isAvailableBasicTimeline(serverMetadata, src.value)) {
+		src.value = availableBasicTimelines(serverMetadata)[0];
 	}
 }
 
@@ -297,7 +299,7 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
 	title: l.name,
 	icon: 'ti ti-star',
 	iconOnly: true,
-}))), ...availableBasicTimelines().map(tl => ({
+}))), ...availableBasicTimelines(serverMetadata).map(tl => ({
 	key: tl,
 	title: i18n.ts._timelines[tl],
 	icon: basicTimelineIconClass(tl),
@@ -319,7 +321,7 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
 	onClick: chooseChannel,
 }] as Tab[]);
 
-const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({
+const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines(serverMetadata).map(tl => ({
 	key: tl,
 	title: i18n.ts._timelines[tl],
 	icon: basicTimelineIconClass(tl),
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 8e0292c7fe..34137b1567 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -151,8 +151,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
+import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref, inject } from 'vue';
 import * as Misskey from 'misskey-js';
+import { getScrollPosition } from '@@/js/scroll.js';
 import MkNote from '@/components/MkNote.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import MkAccountMoved from '@/components/MkAccountMoved.vue';
@@ -161,7 +162,6 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkOmit from '@/components/MkOmit.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getScrollPosition } from '@@/js/scroll.js';
 import { getUserMenu } from '@/scripts/get-user-menu.js';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
@@ -174,7 +174,9 @@ import { confetti } from '@/scripts/confetti.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
 import { useRouter } from '@/router/supplier.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+
+const serverMetadata = inject('serverMetadata');
+const mediaProxy = inject('mediaProxy');
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
@@ -224,7 +226,7 @@ const style = computed(() => {
 	if (props.user.bannerUrl == null) return {};
 	if (defaultStore.state.disableShowingAnimatedImages) {
 		return {
-			backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`,
+			backgroundImage: `url(${ mediaProxy.getStaticImageUrl(props.user.bannerUrl) })`,
 		};
 	} else {
 		return {
@@ -238,7 +240,7 @@ const age = computed(() => {
 });
 
 function menu(ev: MouseEvent) {
-	const { menu, cleanup } = getUserMenu(user.value, router);
+	const { menu, cleanup } = getUserMenu({ serverMetadata }, user.value, router);
 	os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
 }
 
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index ce4d113cad..2cd321e222 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -33,9 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, ref } from 'vue';
+import { inject, onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { notePage } from '@/filters/note.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
@@ -43,6 +42,8 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 
+const mediaProxy = inject('mediaProxy');
+
 const props = defineProps<{
 	user: Misskey.entities.UserDetailed;
 }>();
@@ -56,7 +57,7 @@ const showingFiles = ref<string[]>([]);
 
 function thumbnail(image: Misskey.entities.DriveFile): string {
 	return defaultStore.state.disableShowingAnimatedImages
-		? getStaticImageUrl(image.url)
+		? mediaProxy.getStaticImageUrl(image.url)
 		: image.thumbnailUrl;
 }
 
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index d6ba397f1b..a54043e353 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="meta" class="rsqzvsbo">
+<div class="rsqzvsbo">
 	<MkFeaturedPhotos class="bg"/>
 	<XTimeline class="tl"/>
 	<div class="shape1"></div>
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XTimeline from './welcome.timeline.vue';
 import MarqueeText from '@/components/MkMarquee.vue';
@@ -44,8 +44,9 @@ import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
 import misskeysvg from '/client-assets/misskey.svg';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
-import { instance as meta } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
+const mediaProxy = inject('mediaProxy');
 
 const instances = ref<Misskey.entities.FederationInstance[]>();
 
@@ -53,7 +54,7 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
 	if (!instance.iconUrl) {
 		return '';
 	}
-	return getProxiedImageUrl(instance.iconUrl, 'preview');
+	return mediaProxy.getProxiedImageUrl(instance.iconUrl, 'preview');
 }
 
 misskeyApiGet('federation/instances', {
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 915fe35025..424b375f63 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -17,11 +17,11 @@ import XSetup from './welcome.setup.vue';
 import XEntrance from './welcome.entrance.a.vue';
 import { instanceName } from '@/config.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 
 const instance = ref<Misskey.entities.MetaDetailed | null>(null);
 
-fetchInstance(true).then((res) => {
+fetchServerMetadata(true).then((res) => {
 	instance.value = res;
 });
 
diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts
index ed86529d5b..b5cfe50a02 100644
--- a/packages/frontend/src/scripts/check-permissions.ts
+++ b/packages/frontend/src/scripts/check-permissions.ts
@@ -3,17 +3,17 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { instance } from '@/instance.js';
+import * as Misskey from 'misskey-js';
 import { $i } from '@/account.js';
 
-export const notesSearchAvailable = (
-	// FIXME: instance.policies would be null in Vitest
+export function isNotesSearchAvailable(serverMetadata: Misskey.entities.MetaDetailed): boolean {
+	// FIXME: serverMetadata.policies would be null in Vitest
 	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-	($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
+	return ($i == null && serverMetadata.policies != null && serverMetadata.policies.canSearchNotes) ||
 	($i != null && $i.policies.canSearchNotes) ||
-	false
-) as boolean;
+	false;
+}
 
-export const canSearchNonLocalNotes = (
-	instance.noteSearchableScope === 'global'
-);
+export function canSearchNonLocalNotes(serverMetadata: Misskey.entities.MetaDetailed): boolean {
+	return serverMetadata.noteSearchableScope === 'global';
+}
diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts
index 71d1232710..ec594cc316 100644
--- a/packages/frontend/src/scripts/clear-cache.ts
+++ b/packages/frontend/src/scripts/clear-cache.ts
@@ -7,7 +7,7 @@ import { unisonReload } from '@/scripts/unison-reload.js';
 import * as os from '@/os.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { fetchCustomEmojis } from '@/custom-emojis.js';
-import { fetchInstance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 
 export async function clearCache() {
 	os.waiting();
@@ -18,7 +18,7 @@ export async function clearCache() {
 	miLocalStorage.removeItem('theme');
 	miLocalStorage.removeItem('emojis');
 	miLocalStorage.removeItem('lastEmojisFetchedAt');
-	await fetchInstance(true);
+	await fetchServerMetadata(true);
 	await fetchCustomEmojis(true);
 	unisonReload();
 }
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index e0ccea813d..63e53d0b2e 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -8,7 +8,6 @@ import * as Misskey from 'misskey-js';
 import { claimAchievement } from './achievements.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
@@ -170,7 +169,7 @@ function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuIt
 	};
 }
 
-export function getNoteMenu(props: {
+export function getNoteMenu(ctx: { serverMetadata: Misskey.entities.MetaDetailed }, props: {
 	note: Misskey.entities.Note;
 	translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
 	translating: Ref<boolean>;
@@ -330,7 +329,7 @@ export function getNoteMenu(props: {
 				text: i18n.ts.share,
 				action: share,
 			}] : []),
-			$i && $i.policies.canUseTranslator && instance.translatorAvailable ? {
+			$i && $i.policies.canUseTranslator && ctx.serverMetadata.translatorAvailable ? {
 				icon: 'ti ti-language-hiragana',
 				text: i18n.ts.translate,
 				action: translate,
@@ -375,7 +374,7 @@ export function getNoteMenu(props: {
 				text: i18n.ts.user,
 				children: async () => {
 					const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
-					const { menu, cleanup } = getUserMenu(user);
+					const { menu, cleanup } = getUserMenu(ctx, user);
 					cleanups.push(cleanup);
 					return menu;
 				},
@@ -458,13 +457,13 @@ export function getNoteMenu(props: {
 			text: i18n.ts.copyContent,
 			action: copyContent,
 		}, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink),
-		(appearNote.url || appearNote.uri) ? {
-			icon: 'ti ti-external-link',
-			text: i18n.ts.showOnRemote,
-			action: () => {
-				window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
-			},
-		} : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)]
+										(appearNote.url || appearNote.uri) ? {
+											icon: 'ti ti-external-link',
+											text: i18n.ts.showOnRemote,
+											action: () => {
+												window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
+											},
+										} : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)]
 			.filter(x => x !== undefined);
 	}
 
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 035abc7bd0..5308ecd5ef 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -13,14 +13,14 @@ 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 { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
+import { isNotesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
 import { IRouter } from '@/nirax.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
 import { mainRouter } from '@/router/main.js';
 import { genEmbedCode } from '@/scripts/get-embed-code.js';
 import { MenuItem } from '@/types/menu.js';
 
-export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
+export function getUserMenu(ctx: { serverMetadata: Misskey.entities.MetaDetailed }, user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
 
 	const cleanups = [] as (() => void)[];
@@ -154,7 +154,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		action: () => {
 			copyToClipboard(`@${user.username}@${user.host ?? host}`);
 		},
-	}, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{
+	}, ...(isNotesSearchAvailable(ctx.serverMetadata) && (user.host == null || canSearchNonLocalNotes(ctx.serverMetadata)) ? [{
 		icon: 'ti ti-search',
 		text: i18n.ts.searchThisUsersNotes,
 		action: () => {
diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts
deleted file mode 100644
index facd50ac41..0000000000
--- a/packages/frontend/src/scripts/media-proxy.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { query } from '@@/js/url.js';
-import { url } from '@/config.js';
-import { instance } from '@/instance.js';
-
-export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
-	const localProxy = `${url}/proxy`;
-
-	if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
-		// もう既にproxyっぽそうだったらurlを取り出す
-		imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
-	}
-
-	return `${mustOrigin ? localProxy : instance.mediaProxy}/${
-		type === 'preview' ? 'preview.webp'
-		: 'image.webp'
-	}?${query({
-		url: imageUrl,
-		...(!noFallback ? { 'fallback': '1' } : {}),
-		...(type ? { [type]: '1' } : {}),
-		...(mustOrigin ? { origin: '1' } : {}),
-	})}`;
-}
-
-export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
-	if (imageUrl == null) return null;
-	return getProxiedImageUrl(imageUrl, type);
-}
-
-export function getStaticImageUrl(baseUrl: string): string {
-	const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
-
-	if (u.href.startsWith(`${url}/emoji/`)) {
-		// もう既にemojiっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
-	}
-
-	if (u.href.startsWith(instance.mediaProxy + '/')) {
-		// もう既にproxyっぽそうだったらsearchParams付けるだけ
-		u.searchParams.set('static', '1');
-		return u.href;
-	}
-
-	return `${instance.mediaProxy}/static.webp?${query({
-		url: u.href,
-		static: '1',
-	})}`;
-}
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts
index abb0e1e677..138bb4b044 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -13,7 +13,7 @@ import { apiUrl } from '@/config.js';
 import { $i } from '@/account.js';
 import { alert } from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 
 type Uploading = {
 	id: string;
@@ -40,16 +40,16 @@ export function uploadFile(
 
 	if (folder && typeof folder === 'object') folder = folder.id;
 
-	if (file.size > instance.maxFileSize) {
-		alert({
-			type: 'error',
-			title: i18n.ts.failedToUpload,
-			text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
-		});
-		return Promise.reject();
-	}
+	return fetchServerMetadata().then((serverMetadata) => new Promise((resolve, reject) => {
+		if (file.size > serverMetadata.maxFileSize) {
+			alert({
+				type: 'error',
+				title: i18n.ts.failedToUpload,
+				text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
+			});
+			return reject();
+		}
 
-	return new Promise((resolve, reject) => {
 		const id = uuid();
 
 		const reader = new FileReader();
@@ -158,5 +158,5 @@ export function uploadFile(
 			xhr.send(formData);
 		};
 		reader.readAsArrayBuffer(file);
-	});
+	}));
 }
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/server-metadata.ts
similarity index 51%
rename from packages/frontend/src/instance.ts
rename to packages/frontend/src/server-metadata.ts
index 6847321d6c..470abdd69c 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/server-metadata.ts
@@ -3,11 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { computed, reactive } from 'vue';
 import * as Misskey from 'misskey-js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js';
 
 // TODO: 他のタブと永続化されたstateを同期
 
@@ -26,37 +24,23 @@ if (providedAt > cachedAt) {
 }
 //#endregion
 
-// TODO: instanceをリアクティブにするかは再考の余地あり
-
-export const instance: Misskey.entities.MetaDetailed = reactive(cachedMeta ?? {});
-
-export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
-
-export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO_IMAGE_URL);
-
-export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
-
-export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true);
-
-export async function fetchInstance(force = false): Promise<Misskey.entities.MetaDetailed> {
-	if (!force) {
-		const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
+let metadata: Misskey.entities.MetaDetailed | null = cachedMeta ?? null;
 
+// TODO: 短時間に複数回呼ばれてもリクエストは一回だけにする
+export async function fetchServerMetadata(force = false): Promise<Misskey.entities.MetaDetailed> {
+	if (!force && metadata != null) {
 		if (Date.now() - cachedAt < 1000 * 60 * 60) {
-			return instance;
+			return metadata;
 		}
 	}
 
-	const meta = await misskeyApi('meta', {
+	metadata = await misskeyApi('meta', {
 		detail: true,
 	});
 
-	for (const [k, v] of Object.entries(meta)) {
-		instance[k] = v;
-	}
-
-	miLocalStorage.setItem('instance', JSON.stringify(instance));
+	cachedAt = Date.now();
+	miLocalStorage.setItem('instance', JSON.stringify(metadata));
 	miLocalStorage.setItem('instanceCachedAt', Date.now().toString());
 
-	return instance;
+	return metadata!;
 }
diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts
index 94eda3545e..1e155de472 100644
--- a/packages/frontend/src/timelines.ts
+++ b/packages/frontend/src/timelines.ts
@@ -3,8 +3,8 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+import * as Misskey from 'misskey-js';
 import { $i } from '@/account.js';
-import { instance } from '@/instance.js';
 
 export const basicTimelineTypes = [
 	'home',
@@ -32,23 +32,23 @@ export function basicTimelineIconClass(timeline: BasicTimelineType): string {
 	}
 }
 
-export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean {
+export function isAvailableBasicTimeline(metadata: Misskey.entities.MetaDetailed, timeline: BasicTimelineType | undefined | null): boolean {
 	switch (timeline) {
 		case 'home':
 			return $i != null;
 		case 'local':
-			return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
+			return ($i == null && metadata.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
 		case 'social':
 			return $i != null && $i.policies.ltlAvailable;
 		case 'global':
-			return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
+			return ($i == null && metadata.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
 		default:
 			return false;
 	}
 }
 
-export function availableBasicTimelines(): BasicTimelineType[] {
-	return basicTimelineTypes.filter(isAvailableBasicTimeline);
+export function availableBasicTimelines(metadata: Misskey.entities.MetaDetailed): BasicTimelineType[] {
+	return basicTimelineTypes.filter(timeline => isAvailableBasicTimeline(metadata, timeline));
 }
 
 export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean {
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 74c3028745..1104c5636b 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -6,10 +6,10 @@
 import { defineAsyncComponent } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { instance } from '@/instance.js';
 import { host } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
+import { fetchServerMetadata } from '@/server-metadata.js';
 
 function toolsMenuItems(): MenuItem[] {
 	return [{
@@ -40,9 +40,10 @@ function toolsMenuItems(): MenuItem[] {
 	} : undefined];
 }
 
-export function openInstanceMenu(ev: MouseEvent) {
+export async function openInstanceMenu(ev: MouseEvent) {
+	const serverMetadata = await fetchServerMetadata();
 	os.popupMenu([{
-		text: instance.name ?? host,
+		text: serverMetadata.name ?? host,
 		type: 'label',
 	}, {
 		type: 'link',
@@ -69,7 +70,7 @@ export function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.ads,
 		icon: 'ti ti-ad',
 		to: '/ads',
-	}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
+	}, ($i && ($i.isAdmin || $i.policies.canInvite) && serverMetadata.disableRegistration) ? {
 		type: 'link',
 		to: '/invite',
 		text: i18n.ts.invite,
@@ -84,25 +85,25 @@ export function openInstanceMenu(ev: MouseEvent) {
 		text: i18n.ts.inquiry,
 		icon: 'ti ti-help-circle',
 		to: '/contact',
-	}, (instance.impressumUrl) ? {
+	}, (serverMetadata.impressumUrl) ? {
 		type: 'a',
 		text: i18n.ts.impressum,
 		icon: 'ti ti-file-invoice',
-		href: instance.impressumUrl,
+		href: serverMetadata.impressumUrl,
 		target: '_blank',
-	} : undefined, (instance.tosUrl) ? {
+	} : undefined, (serverMetadata.tosUrl) ? {
 		type: 'a',
 		text: i18n.ts.termsOfService,
 		icon: 'ti ti-notebook',
-		href: instance.tosUrl,
+		href: serverMetadata.tosUrl,
 		target: '_blank',
-	} : undefined, (instance.privacyPolicyUrl) ? {
+	} : undefined, (serverMetadata.privacyPolicyUrl) ? {
 		type: 'a',
 		text: i18n.ts.privacyPolicy,
 		icon: 'ti ti-shield-lock',
-		href: instance.privacyPolicyUrl,
+		href: serverMetadata.privacyPolicyUrl,
 		target: '_blank',
-	} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
+	} : undefined, (!serverMetadata.impressumUrl && !serverMetadata.tosUrl && !serverMetadata.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
 		type: 'a',
 		text: i18n.ts.document,
 		icon: 'ti ti-bulb',
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index e80d5fd399..96e7b504ae 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<div :class="$style.top">
-		<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
+		<div :class="$style.banner" :style="{ backgroundImage: `url(${ serverMetadata.bannerUrl })` }"></div>
 		<button class="_button" :class="$style.instance" @click="openInstanceMenu">
-			<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
+			<img :src="serverMetadata.iconUrl || serverMetadata.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
 		</button>
 	</div>
 	<div :class="$style.middle">
@@ -49,14 +49,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, toRef } from 'vue';
+import { computed, defineAsyncComponent, inject, toRef } from 'vue';
 import { openInstanceMenu } from './common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const menu = toRef(defaultStore.state, 'menu');
 const otherMenuItemIndicated = computed(() => {
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 2b116909ba..e71c4fefe4 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="[$style.root, { [$style.iconOnly]: iconOnly }]">
 	<div :class="$style.body">
 		<div :class="$style.top">
-			<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
-			<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
-				<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
+			<div :class="$style.banner" :style="{ backgroundImage: `url(${ serverMetadata.bannerUrl })` }"></div>
+			<button v-tooltip.noDelay.right="serverMetadata.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
+				<img :src="serverMetadata.iconUrl || serverMetadata.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
 			</button>
 		</div>
 		<div :class="$style.middle">
@@ -60,14 +60,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, inject, ref, watch } from 'vue';
 import { openInstanceMenu } from './common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const iconOnly = ref(false);
 
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index e234bb3a33..d5dc8f4123 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -31,12 +31,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+
+const mediaProxy = inject('mediaProxy');
 
 const props = defineProps<{
 	display?: 'marquee' | 'oneByOne';
@@ -68,7 +69,7 @@ useInterval(tick, Math.max(5000, props.refreshIntervalSec * 1000), {
 });
 
 function getInstanceIcon(instance): string {
-	return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
+	return mediaProxy.getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? mediaProxy.getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
 }
 </script>
 
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index c03afd6cd6..157c848de8 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="body">
 		<div class="left">
 			<button v-click-anime class="item _button instance" @click="openInstanceMenu">
-				<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
+				<img :src="serverMetadata.iconUrl ?? serverMetadata.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
 			</button>
 			<MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact>
 				<i class="ti ti-home ti-fw"></i>
@@ -47,16 +47,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
+import { computed, defineAsyncComponent, inject, onMounted, ref } from 'vue';
 import { openInstanceMenu } from './_common_/common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { openAccountMenu as openAccountMenu_, $i } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store.js';
-import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const WINDOW_THRESHOLD = 1400;
 
 const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index d8574a915f..fbcb78b397 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="divider"></div>
 	<div class="about">
 		<button v-click-anime class="item _button" @click="openInstanceMenu">
-			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
+			<img :src="serverMetadata.iconUrl ?? serverMetadata.faviconUrl ?? '/favicon.ico'" class="_ghost"/>
 		</button>
 	</div>
 	<!--<MisskeyLogo class="misskey"/>-->
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { defineAsyncComponent, computed, watch, ref, shallowRef } from 'vue';
+import { defineAsyncComponent, computed, watch, ref, shallowRef, inject } from 'vue';
 import { openInstanceMenu } from './_common_/common.js';
 // import { host } from '@/config.js';
 import * as os from '@/os.js';
@@ -60,9 +60,10 @@ import MkButton from '@/components/MkButton.vue';
 // import { mainRouter } from '@/router.js';
 //import MisskeyLogo from '@assets/client/misskey.svg';
 import { defaultStore } from '@/store.js';
-import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const WINDOW_THRESHOLD = 1400;
 
 const menu = ref(defaultStore.state.menu);
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index e210ee7b7a..eef9abf849 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
-	<div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled">
+	<div v-if="!isAvailableBasicTimeline(serverMetadata, column.tl)" :class="$style.disabled">
 		<p :class="$style.disabledTitle">
 			<i class="ti ti-circle-minus"></i>
 			{{ i18n.ts._disabledTimeline.title }}
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, watch, ref, shallowRef, computed } from 'vue';
+import { onMounted, watch, ref, shallowRef, computed, inject } from 'vue';
 import XColumn from './column.vue';
 import { removeColumn, updateColumn, Column } from './deck-store.js';
 import type { MenuItem } from '@/types/menu.js';
@@ -39,11 +39,12 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
-import { instance } from '@/instance.js';
 import { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
 import * as sound from '@/scripts/sound.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const props = defineProps<{
 	column: Column;
 	isStacked: boolean;
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index c229946bd4..455961c7f6 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
 
 	<div v-if="!narrow && !isRoot" class="side">
-		<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
+		<div class="banner" :style="{ backgroundImage: serverMetadata.backgroundImageUrl ? `url(${ serverMetadata.backgroundImageUrl })` : 'none' }"></div>
 		<div class="dashboard">
 			<MkVisitorDashboard/>
 		</div>
@@ -69,11 +69,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, provide, ref, computed } from 'vue';
+import { onMounted, provide, ref, computed, inject } from 'vue';
 import XCommon from './_common_/common.vue';
 import { instanceName } from '@/config.js';
 import * as os from '@/os.js';
-import { instance } from '@/instance.js';
 import XSigninDialog from '@/components/MkSigninDialog.vue';
 import XSignupDialog from '@/components/MkSignupDialog.vue';
 import { ColdDeviceStorage, defaultStore } from '@/store.js';
@@ -82,6 +81,8 @@ import { i18n } from '@/i18n.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
 import { mainRouter } from '@/router/main.js';
 
+const serverMetadata = inject('serverMetadata');
+
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 
 const DESKTOP_THRESHOLD = 1100;
@@ -107,7 +108,7 @@ const announcements = {
 	limit: 10,
 };
 
-const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable);
+const isTimelineAvailable = ref(serverMetadata.policies?.ltlAvailable || serverMetadata.policies?.gtlAvailable);
 
 const showMenu = ref(false);
 const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index bcfaaf00ab..34ef3b90dc 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAvatar v-for="user in users" :key="user.id" :user="user.followee" link preview></MkAvatar>
 		</div>
 		<div v-else :class="$style.bdayFFallback">
-			<img :src="infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost" :class="$style.bdayFFallbackImage"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 	</div>
@@ -31,7 +31,8 @@ import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 import { $i } from '@/account.js';
 
 const name = i18n.ts._widgets.birthdayFollowings;
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index c10416e4fb..507f488a92 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -25,16 +25,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 import { defaultStore } from '@/store.js';
 
 const name = 'federation';
@@ -57,6 +56,8 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
+const mediaProxy = inject('mediaProxy');
+
 const instances = ref<Misskey.entities.FederationInstance[]>([]);
 const charts = ref<Misskey.entities.ChartsInstanceResponse[]>([]);
 const fetching = ref(true);
@@ -78,7 +79,7 @@ useInterval(fetch, 1000 * 60, {
 });
 
 function getInstanceIcon(instance): string {
-	return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
+	return mediaProxy.getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? mediaProxy.getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
 }
 
 defineExpose<WidgetComponentExpose>({
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index d090372b9a..3712f2911e 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -18,16 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { inject, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkTagCloud from '@/components/MkTagCloud.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 
 const name = 'instanceCloud';
 
@@ -49,6 +48,8 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
+const mediaProxy = inject('mediaProxy');
+
 const cloud = shallowRef<InstanceType<typeof MkTagCloud> | null>();
 const activeInstances = shallowRef<Misskey.entities.FederationInstance[] | null>(null);
 
@@ -70,7 +71,7 @@ useInterval(() => {
 });
 
 function getInstanceIcon(instance): string {
-	return getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
+	return mediaProxy.getProxiedImageUrlNullable(instance.iconUrl, 'preview') ?? mediaProxy.getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? '/client-assets/dummy.png';
 }
 
 defineExpose<WidgetComponentExpose>({
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 5d8beaf9a9..d2a0a64c41 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_panel">
-	<div :class="$style.container" :style="{ backgroundImage: instance.bannerUrl ? `url(${ instance.bannerUrl })` : null }">
+	<div :class="$style.container" :style="{ backgroundImage: serverMetadata.bannerUrl ? `url(${ serverMetadata.bannerUrl })` : null }">
 		<div :class="$style.iconContainer">
-			<img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
+			<img :src="serverMetadata.iconUrl ?? serverMetadata.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
 		</div>
 		<div :class="$style.bodyContainer">
 			<div :class="$style.body">
-				<MkA :class="$style.name" to="/about" behavior="window">{{ instance.name }}</MkA>
+				<MkA :class="$style.name" to="/about" behavior="window">{{ serverMetadata.name }}</MkA>
 				<div :class="$style.host">{{ host }}</div>
 			</div>
 		</div>
@@ -20,10 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import { inject } from 'vue';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import { host } from '@/config.js';
-import { instance } from '@/instance.js';
+
+const serverMetadata = inject('serverMetadata');
 
 const name = 'instanceInfo';
 
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 34be8c5e57..64b3b83c29 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -22,12 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onUnmounted, ref } from 'vue';
+import { inject, onUnmounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import { useStream } from '@/stream.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { defaultStore } from '@/store.js';
@@ -57,6 +56,8 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
+const mediaProxy = inject('mediaProxy');
+
 const connection = useStream().useChannel('main');
 const images = ref<Misskey.entities.DriveFile[]>([]);
 const fetching = ref(true);
@@ -70,7 +71,7 @@ const onDriveFileCreated = (file) => {
 
 const thumbnail = (image: any): string => {
 	return defaultStore.state.disableShowingAnimatedImages
-		? getStaticImageUrl(image.url)
+		? mediaProxy.getStaticImageUrl(image.url)
 		: image.thumbnailUrl;
 };
 
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 13f5a4802a..457b6b6bc6 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="ekmkgxbj">
 		<MkLoading v-if="fetching"/>
 		<div v-else-if="(!items || items.length === 0) && widgetProps.showHeader" class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
+			<img v-if="serverMetadata.infoImageUrl" :src="serverMetadata.infoImageUrl" class="_ghost"/>
 			<div>{{ i18n.ts.nothing }}</div>
 		</div>
 		<div v-else :class="$style.feed">
@@ -31,7 +31,8 @@ import MkContainer from '@/components/MkContainer.vue';
 import { url as base } from '@/config.js';
 import { i18n } from '@/i18n.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { infoImageUrl } from '@/instance.js';
+import { inject } from 'vue';
+const serverMetadata = inject('serverMetadata');
 
 const name = 'rss';
 
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index d02f9b8e22..57af0e00eb 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</button>
 	</template>
 
-	<div v-if="isBasicTimeline(widgetProps.src) && !isAvailableBasicTimeline(widgetProps.src)" :class="$style.disabled">
+	<div v-if="isBasicTimeline(widgetProps.src) && !isAvailableBasicTimeline(serverMetadata, widgetProps.src)" :class="$style.disabled">
 		<p :class="$style.disabledTitle">
 			<i class="ti ti-minus"></i>
 			{{ i18n.ts._disabledTimeline.title }}
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import { GetFormResultType } from '@/scripts/form.js';
 import * as os from '@/os.js';
@@ -80,6 +80,8 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
 	emit,
 );
 
+const serverMetadata = inject('serverMetadata');
+
 const menuOpened = ref(false);
 
 const setSrc = (src) => {
@@ -109,7 +111,7 @@ const choose = async (ev) => {
 			setSrc('list');
 		},
 	}));
-	os.popupMenu([...availableBasicTimelines().map(tl => ({
+	os.popupMenu([...availableBasicTimelines(serverMetadata).map(tl => ({
 		text: i18n.ts._timelines[tl],
 		icon: basicTimelineIconClass(tl),
 		action: () => { setSrc(tl); },