diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89f366ecc9..5ca658347d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
 ChangeLog
 =========
 
+unreleased
+----------
+* Pleromaとのフェデレーションを修正
+* インスタンスのキャラクター画像を設定できるように
+* Catモードの朝鮮語対応
+* CWが付いた投稿に返信する際、そのCWを引き継ぐように
+
 10.73.0
 -------
 * テーマの強化
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 89f8236c7d..31741af150 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -992,6 +992,7 @@ admin/views/instance.vue:
   instance-name: "Instance name"
   instance-description: "Instance description"
   host: "Host"
+  logo-url: "Logo image URL"
   banner-url: "Banner image URL"
   error-image-url: "Error image URL"
   languages: "Language of this instance"
diff --git a/package.json b/package.json
index 6479252868..b6517135dc 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
 	},
 	"dependencies": {
 		"@fortawesome/fontawesome-svg-core": "1.2.12",
-		"@fortawesome/free-brands-svg-icons": "5.6.0",
+		"@fortawesome/free-brands-svg-icons": "5.6.3",
 		"@fortawesome/free-regular-svg-icons": "5.5.0",
 		"@fortawesome/free-solid-svg-icons": "5.6.3",
 		"@fortawesome/vue-fontawesome": "0.1.2",
@@ -34,7 +34,7 @@
 		"@types/debug": "0.0.31",
 		"@types/deep-equal": "1.0.1",
 		"@types/double-ended-queue": "2.1.0",
-		"@types/elasticsearch": "5.0.29",
+		"@types/elasticsearch": "5.0.30",
 		"@types/file-type": "10.6.0",
 		"@types/gulp": "3.8.36",
 		"@types/gulp-mocha": "0.0.32",
@@ -46,7 +46,7 @@
 		"@types/is-url": "1.2.28",
 		"@types/js-yaml": "3.11.4",
 		"@types/katex": "0.5.0",
-		"@types/koa": "2.0.47",
+		"@types/koa": "2.0.48",
 		"@types/koa-bodyparser": "5.0.2",
 		"@types/koa-compress": "2.0.8",
 		"@types/koa-favicon": "2.0.19",
@@ -70,7 +70,7 @@
 		"@types/pug": "2.0.4",
 		"@types/qrcode": "1.3.0",
 		"@types/ratelimiter": "2.1.28",
-		"@types/redis": "2.8.8",
+		"@types/redis": "2.8.10",
 		"@types/request": "2.48.1",
 		"@types/request-promise-native": "1.0.15",
 		"@types/rimraf": "2.0.2",
@@ -87,7 +87,7 @@
 		"@types/websocket": "0.0.40",
 		"@types/ws": "6.0.1",
 		"animejs": "2.2.0",
-		"apexcharts": "2.4.2",
+		"apexcharts": "2.5.1",
 		"autobind-decorator": "2.4.0",
 		"autosize": "4.0.2",
 		"autwh": "0.1.0",
@@ -116,7 +116,7 @@
 		"eventemitter3": "3.1.0",
 		"feed": "2.0.2",
 		"file-loader": "2.0.0",
-		"file-type": "10.6.0",
+		"file-type": "10.7.0",
 		"fuckadblock": "3.2.1",
 		"gulp": "3.9.1",
 		"gulp-cssnano": "2.1.3",
@@ -222,7 +222,7 @@
 		"vue-color": "2.7.0",
 		"vue-content-loading": "1.5.3",
 		"vue-cropperjs": "3.0.0",
-		"vue-i18n": "8.3.2",
+		"vue-i18n": "8.6.0",
 		"vue-js-modal": "1.3.28",
 		"vue-loader": "15.4.2",
 		"vue-marquee-text-component": "1.1.0",
@@ -237,7 +237,7 @@
 		"vuex-persistedstate": "2.5.4",
 		"web-push": "3.3.3",
 		"webfinger.js": "2.7.0",
-		"webpack": "4.26.1",
+		"webpack": "4.28.3",
 		"webpack-cli": "3.1.2",
 		"websocket": "1.0.28",
 		"ws": "6.1.2",
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue
index 5a29da0345..6fd92a779f 100644
--- a/src/client/app/admin/views/instance.vue
+++ b/src/client/app/admin/views/instance.vue
@@ -6,6 +6,7 @@
 			<ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
 			<ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
 			<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
+			<ui-input v-model="mascotImageUrl"><i slot="icon"><fa icon="link"/></i>{{ $t('logo-url') }}</ui-input>
 			<ui-input v-model="bannerUrl"><i slot="icon"><fa icon="link"/></i>{{ $t('banner-url') }}</ui-input>
 			<ui-input v-model="errorImageUrl"><i slot="icon"><fa icon="link"/></i>{{ $t('error-image-url') }}</ui-input>
 			<ui-input v-model="languages"><i slot="icon"><fa icon="language"/></i>{{ $t('languages') }}<span slot="desc">{{ $t('languages-desc') }}</span></ui-input>
@@ -149,6 +150,7 @@ export default Vue.extend({
 			maintainerEmail: null,
 			disableRegistration: false,
 			disableLocalTimeline: false,
+			mascotImageUrl: null,
 			bannerUrl: null,
 			errorImageUrl: null,
 			name: null,
@@ -196,6 +198,7 @@ export default Vue.extend({
 			this.maintainerEmail = meta.maintainer.email;
 			this.disableRegistration = meta.disableRegistration;
 			this.disableLocalTimeline = meta.disableLocalTimeline;
+			this.mascotImageUrl = meta.mascotImageUrl;
 			this.bannerUrl = meta.bannerUrl;
 			this.errorImageUrl = meta.errorImageUrl;
 			this.name = meta.name;
@@ -253,6 +256,7 @@ export default Vue.extend({
 				maintainerEmail: this.maintainerEmail,
 				disableRegistration: this.disableRegistration,
 				disableLocalTimeline: this.disableLocalTimeline,
+				mascotImageUrl: this.mascotImageUrl,
 				bannerUrl: this.bannerUrl,
 				errorImageUrl: this.errorImageUrl,
 				name: this.name,
diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue
index 69259ab60f..2730e4139c 100644
--- a/src/client/app/common/views/components/acct.vue
+++ b/src/client/app/common/views/components/acct.vue
@@ -2,6 +2,7 @@
 <span class="mk-acct">
 	<span class="name">@{{ user.username }}</span>
 	<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
+	<fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/>
 </span>
 </template>
 
@@ -23,4 +24,8 @@ export default Vue.extend({
 .mk-acct
 	> .host.fade
 		opacity 0.5
+
+	> .locked
+		opacity 0.8
+		margin-left 0.5em
 </style>
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 038541ba05..958abe00f0 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -8,16 +8,16 @@
 	</blockquote>
 </div>
 <div v-else class="mk-url-preview">
-	<a :class="{ mini }" :href="url" target="_blank" :title="url" v-if="!fetching">
+	<a :class="{ mini, compact }" :href="url" target="_blank" :title="url" v-if="!fetching">
 		<div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div>
 		<article>
 			<header>
-				<h1>{{ title }}</h1>
+				<h1 :title="title">{{ title }}</h1>
 			</header>
-			<p v-if="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
+			<p v-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
 			<footer>
 				<img class="icon" v-if="icon" :src="icon"/>
-				<p>{{ sitename }}</p>
+				<p :title="sitename">{{ sitename }}</p>
 			</footer>
 		</article>
 	</a>
@@ -120,6 +120,12 @@ export default Vue.extend({
 			default: false
 		},
 
+		compact: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
+
 		mini: {
 			type: Boolean,
 			required: false,
@@ -302,6 +308,23 @@ export default Vue.extend({
 						width 12px
 						height 12px
 
+			&.compact
+				> .thumbnail
+					position: absolute
+					width 56px
+					height 100%
+
+				> article
+					left 56px
+					width calc(100% - 56px)
+					padding 4px
+
+					> header
+						margin-bottom 2px
+
+					> footer
+						margin-top 2px
+
 		&.mini
 			font-size 10px
 
@@ -325,4 +348,27 @@ export default Vue.extend({
 						width 12px
 						height 12px
 
+			&.compact
+				> .thumbnail
+					position: absolute
+					width 56px
+					height 100%
+
+				> article
+					left 56px
+					width calc(100% - 56px)
+					padding 4px
+
+					> header
+						margin-bottom 2px
+
+					> footer
+						margin-top 2px
+
+		&.compact
+			> article
+				> header h1, p, footer
+					overflow: hidden;
+					white-space: nowrap;
+					text-overflow: ellipsis;
 </style>
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index a52eaac7e9..e5f233a552 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -36,7 +36,7 @@
 					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
 					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> 位置情報</a>
 					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote" :mini="mini"/></div>
-					<mk-url-preview v-for="url in urls" :url="url" :key="url" :mini="mini"/>
+					<mk-url-preview v-for="url in urls" :url="url" :key="url" :mini="mini" :compact="compact"/>
 				</div>
 			</div>
 			<footer v-if="appearNote.deletedAt == null">
@@ -102,6 +102,11 @@ export default Vue.extend({
 			required: false,
 			default: false
 		},
+		compact: {
+			type: Boolean,
+			required: false,
+			default: false
+		},
 		mini: {
 			type: Boolean,
 			required: false,
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index da11893225..5cf51d9cc4 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -15,7 +15,7 @@
 	<!-- トランジションを有効にするとなぜかメモリリークする -->
 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
 		<template v-for="(note, i) in _notes">
-			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
+			<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :compact="true" ref="note"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
 				<span><fa icon="angle-up"/>{{ note._datetext }}</span>
 				<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index 98c16cd5d0..27e487946b 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -77,6 +77,7 @@ import extractMentions from '../../../../../misc/extract-mentions';
 
 export default Vue.extend({
 	i18n: i18n('desktop/views/components/post-form.vue'),
+
 	components: {
 		XDraggable,
 		MkVisibilityChooser
@@ -197,11 +198,11 @@ export default Vue.extend({
 				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
 
 				// 自分は除外
-				if (this.$store.state.i.username == x.username && x.host == null) return;
-				if (this.$store.state.i.username == x.username && x.host == host) return;
+				if (this.$store.state.i.username == x.username && x.host == null) continue;
+				if (this.$store.state.i.username == x.username && x.host == host) continue;
 
 				// 重複は除外
-				if (this.text.indexOf(`${mention} `) != -1) return;
+				if (this.text.indexOf(`${mention} `) != -1) continue;
 
 				this.text += `${mention} `;
 			}
@@ -221,6 +222,12 @@ export default Vue.extend({
 			});
 		}
 
+		// keep cw when reply
+		if (this.reply && this.reply.cw != null) {
+			this.useCw = true;
+			this.cw = this.reply.cw;
+		}
+
 		this.$nextTick(() => {
 			// 書きかけの投稿を復元
 			if (!this.instant && !this.mention) {
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index e6a09c3eea..d8f38bedfc 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -21,7 +21,14 @@ import i18n from '../../../i18n';
 
 export default Vue.extend({
 	i18n: i18n('desktop/views/components/renote-form.vue'),
-	props: ['note'],
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		}
+	},
+
 	data() {
 		return {
 			wait: false,
@@ -29,6 +36,7 @@ export default Vue.extend({
 			visibility: this.$store.state.settings.defaultNoteVisibility
 		};
 	},
+
 	methods: {
 		ok(v: string) {
 			this.wait = true;
@@ -44,9 +52,11 @@ export default Vue.extend({
 				this.wait = false;
 			});
 		},
+
 		cancel() {
 			this.$emit('canceled');
 		},
+
 		onQuote() {
 			this.quote = true;
 
@@ -54,6 +64,7 @@ export default Vue.extend({
 				(this.$refs.form as any).focus();
 			});
 		},
+
 		onChildFormPosted() {
 			this.$emit('posted');
 		}
diff --git a/src/client/app/desktop/views/components/user-card.vue b/src/client/app/desktop/views/components/user-card.vue
index c59467fd09..6a42b5da00 100644
--- a/src/client/app/desktop/views/components/user-card.vue
+++ b/src/client/app/desktop/views/components/user-card.vue
@@ -7,7 +7,8 @@
 		<router-link :to="user | userPage" class="name">
 			<mk-user-name :user="user"/>
 		</router-link>
-		<span class="username">@{{ user | acct }}</span>
+		<span class="username">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span>
+		
 		<div class="description">
 			<misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
 		</div>
@@ -75,6 +76,9 @@ export default Vue.extend({
 			display block
 			opacity 0.7
 
+			> .locked
+				opacity 0.8
+
 		> .description
 			margin 8px 0 16px 0
 
diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue
index 994e12a04b..54e01a0012 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notes.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -18,6 +18,7 @@
 				:key="note.id"
 				@update:note="onNoteUpdated(i, $event)"
 				:media-view="mediaView"
+				:compact="true"
 				:mini="true"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
 				<span><fa icon="angle-up"/>{{ note._datetext }}</span>
diff --git a/src/client/app/desktop/views/pages/deck/deck.user-column.vue b/src/client/app/desktop/views/pages/deck/deck.user-column.vue
index 0ed7916a4b..65feea9693 100644
--- a/src/client/app/desktop/views/pages/deck/deck.user-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.user-column.vue
@@ -19,7 +19,7 @@
 				<span class="name">
 					<mk-user-name :user="user"/>
 				</span>
-				<span class="acct">@{{ user | acct }}</span>
+				<span class="acct">@{{ user | acct }} <fa v-if="user.isLocked == true" class="locked" icon="lock" fixed-width/></span>
 			</div>
 		</header>
 		<div class="info">
@@ -411,6 +411,9 @@ export default Vue.extend({
 				opacity 0.7
 				text-shadow 0 0 8px #000
 
+				> .locked
+					opacity 0.8
+
 	> .info
 		padding 16px
 		font-size 12px
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 8abadf2c69..88a11eafa6 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -35,7 +35,7 @@
 						<span class="signin" @click="signin">{{ $t('signin') }}</span>
 					</p>
 
-					<img src="/assets/ai.png" alt="" title="藍" class="char">
+					<img :src="meta.mascotImageUrl" alt="" title="藍" class="char">
 				</div>
 			</div>
 
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 57d8fb2a08..1533e0a62a 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -30,7 +30,7 @@
 						<mk-media-list :media-list="appearNote.files"/>
 					</div>
 					<mk-poll v-if="appearNote.poll" :note="appearNote" ref="pollViewer"/>
-					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+					<mk-url-preview v-for="url in urls" :url="url" :key="url" :compact="compact"/>
 					<a class="location" v-if="appearNote.geo" :href="`https://maps.google.com/maps?q=${appearNote.geo.coordinates[1]},${appearNote.geo.coordinates[0]}`" target="_blank"><fa icon="map-marker-alt"/> {{ $t('location') }}</a>
 					<div class="renote" v-if="appearNote.renote"><mk-note-preview :note="appearNote.renote"/></div>
 				</div>
@@ -90,6 +90,11 @@ export default Vue.extend({
 		note: {
 			type: Object,
 			required: true
+		},
+		compact: {
+			type: Boolean,
+			required: false,
+			default: false
 		}
 	}
 });
diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue
index fe88fee84d..1d0375cfa9 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -15,7 +15,7 @@
 	<!-- トランジションを有効にするとなぜかメモリリークする -->
 	<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
 		<template v-for="(note, i) in _notes">
-			<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+			<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" :compact="true"/>
 			<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
 				<span><fa icon="angle-up"/>{{ note._datetext }}</span>
 				<span><fa icon="angle-down"/>{{ _notes[i + 1]._datetext }}</span>
diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue
index 00d0eebd6a..47af21c2a5 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -187,11 +187,11 @@ export default Vue.extend({
 				const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
 
 				// 自分は除外
-				if (this.$store.state.i.username == x.username && x.host == null) return;
-				if (this.$store.state.i.username == x.username && x.host == host) return;
+				if (this.$store.state.i.username == x.username && x.host == null) continue;
+				if (this.$store.state.i.username == x.username && x.host == host) continue;
 
 				// 重複は除外
-				if (this.text.indexOf(`${mention} `) != -1) return;
+				if (this.text.indexOf(`${mention} `) != -1) continue;
 
 				this.text += `${mention} `;
 			}
@@ -211,6 +211,12 @@ export default Vue.extend({
 			});
 		}
 
+		// keep cw when reply
+		if (this.reply && this.reply.cw != null) {
+			this.useCw = true;
+			this.cw = this.reply.cw;
+		}
+
 		this.focus();
 
 		this.$nextTick(() => {
diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts
index a49e4dbe78..e6488da395 100644
--- a/src/misc/fetch-meta.ts
+++ b/src/misc/fetch-meta.ts
@@ -19,6 +19,7 @@ const defaultMeta: any = {
 	enableExternalUserRecommendation: false,
 	externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}',
 	externalUserRecommendationTimeout: 300000,
+	mascotImageUrl: '/assets/ai.png',
 	errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png',
 	enableServiceWorker: false
 };
diff --git a/src/models/meta.ts b/src/models/meta.ts
index fd07438a81..daa7964d6f 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -185,6 +185,7 @@ export type IMeta = {
 	disableRegistration?: boolean;
 	disableLocalTimeline?: boolean;
 	hidedTags?: string[];
+	mascotImageUrl?: string;
 	bannerUrl?: string;
 	errorImageUrl?: string;
 
diff --git a/src/models/note.ts b/src/models/note.ts
index f334970750..f2fb39051b 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -372,7 +372,14 @@ export const pack = async (
 	//#endregion
 
 	if (_note.user.isCat && _note.text) {
-		_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ');
+		_note.text = (_note.text
+			// ja-JP
+			.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
+			// ko-KR
+			.replace(/[나-낳]/g, (match: string) => String.fromCharCode(
+				match.codePointAt(0)  + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
+			))
+		);
 	}
 
 	if (!opts.skipHide) {
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 42d90ab91f..9adc3dd943 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -83,7 +83,7 @@ router.get('/notes/:note', async (ctx, next) => {
 	}
 
 	ctx.body = pack(await renderNote(note, false));
-	ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+	ctx.set('Cache-Control', 'public, max-age=180');
 	setResponseType(ctx);
 });
 
@@ -162,7 +162,9 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) {
 	setResponseType(ctx);
 }
 
-router.get('/users/:user', async ctx => {
+router.get('/users/:user', async (ctx, next) => {
+	if (!isActivityPubReq(ctx)) return await next();
+
 	if (!ObjectID.isValid(ctx.params.user)) {
 		ctx.status = 404;
 		return;
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index 6ceb2a98dc..cf117ebd71 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -39,6 +39,13 @@ export const meta = {
 			}
 		},
 
+		mascotImageUrl: {
+			validator: $.str.optional.nullable,
+			desc: {
+				'ja-JP': 'インスタンスキャラクター画像のURL'
+			}
+		},
+
 		bannerUrl: {
 			validator: $.str.optional.nullable,
 			desc: {
@@ -328,6 +335,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
 		set.hidedTags = ps.hidedTags;
 	}
 
+	if (ps.mascotImageUrl !== undefined) {
+		set.mascotImageUrl = ps.mascotImageUrl;
+	}
+
 	if (ps.bannerUrl !== undefined) {
 		set.bannerUrl = ps.bannerUrl;
 	}
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index d406f59dc9..fa933513b7 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -65,6 +65,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 		enableRecaptcha: instance.enableRecaptcha,
 		recaptchaSiteKey: instance.recaptchaSiteKey,
 		swPublickey: instance.swPublicKey,
+		mascotImageUrl: instance.mascotImageUrl,
 		bannerUrl: instance.bannerUrl,
 		errorImageUrl: instance.errorImageUrl,
 		maxNoteTextLength: instance.maxNoteTextLength,
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index c9f70d9658..6c90898250 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -45,7 +45,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
 	}
 
 	if (note.deletedAt != null) {
-		return rej('this not is already deleted');
+		return rej('this note is already deleted');
 	}
 
 	try {
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 998fd2adf2..59b8390ab5 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -148,6 +148,27 @@ router.get('/@:user', async (ctx, next) => {
 	}
 });
 
+router.get('/users/:user', async ctx => {
+	if (!ObjectID.isValid(ctx.params.user)) {
+		ctx.status = 404;
+		return;
+	}
+
+	const userId = new ObjectID(ctx.params.user);
+
+	const user = await User.findOne({
+		_id: userId,
+		host: null
+	});
+
+	if (user === null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
+});
+
 // Note
 router.get('/notes/:note', async ctx => {
 	if (ObjectID.isValid(ctx.params.note)) {
@@ -159,7 +180,12 @@ router.get('/notes/:note', async ctx => {
 				note: _note,
 				summary: getNoteSummary(_note)
 			});
-			ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+
+			if (['public', 'home'].includes(note.visibility)) {
+				ctx.set('Cache-Control', 'public, max-age=180');
+			} else {
+				ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
+			}
 
 			return;
 		}