diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee2d39cdef..8f895f8009 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,7 +30,8 @@
 - Enhance: ストリーミングAPIのパフォーマンスを向上
 - Fix: users/notesでDBから参照した際にチャンネル投稿のみ取得される問題を修正
 - Fix: コントロールパネルの設定項目が正しく保存できない問題を修正
-- Change: nyaizeはAPIレスポンス時ではなく投稿時に一度だけ非可逆的に行われるようになりました
+- Change: ユーザーのisCatがtrueでも、サーバーではnyaizeが行われなくなりました
+  - isCatな場合、クライアントでnyaize処理を行うことを推奨します
 
 ## 2023.10.1
 ### General
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 2c00418c47..f5cfe03122 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -227,8 +227,6 @@ export class NoteCreateService implements OnApplicationShutdown {
 		isBot: MiUser['isBot'];
 		isCat: MiUser['isCat'];
 	}, data: Option, silent = false): Promise<MiNote> {
-		let patsedText: mfm.MfmNode[] | null = null;
-
 		// チャンネル外にリプライしたら対象のスコープに合わせる
 		// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
 		if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@@ -315,25 +313,6 @@ export class NoteCreateService implements OnApplicationShutdown {
 				data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
 			}
 			data.text = data.text.trim();
-
-			if (user.isCat) {
-				patsedText = mfm.parse(data.text);
-				function nyaizeNode(node: mfm.MfmNode) {
-					if (node.type === 'quote') return;
-					if (node.type === 'text') {
-						node.props.text = nyaize(node.props.text);
-					}
-					if (node.children) {
-						for (const child of node.children) {
-							nyaizeNode(child);
-						}
-					}
-				}
-				for (const node of patsedText) {
-					nyaizeNode(node);
-				}
-				data.text = mfm.toString(patsedText);
-			}
 		} else {
 			data.text = null;
 		}
@@ -344,7 +323,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 		// Parse MFM if needed
 		if (!tags || !emojis || !mentionedUsers) {
-			const tokens = patsedText ?? (data.text ? mfm.parse(data.text)! : []);
+			const tokens = (data.text ? mfm.parse(data.text)! : []);
 			const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
 			const choiceTokens = data.poll && data.poll.choices
 				? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index 2ae3fc89c8..ea3655f6bb 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -17,6 +17,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
 import MkA from '@/components/global/MkA.vue';
 import { host } from '@/config.js';
 import { defaultStore } from '@/store.js';
+import { nyaize } from '@/scripts/nyaize.js';
 
 const QUOTE_STYLE = `
 display: block;
@@ -55,10 +56,13 @@ export default function(props: {
 	 * @param ast MFM AST
 	 * @param scale How times large the text is
 	 */
-	const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
+	const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
 		switch (token.type) {
 			case 'text': {
-				const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+				let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+				if (!disableNyaize && props.author.isCat) {
+					text = nyaize(text);
+				}
 
 				if (!props.plain) {
 					const res: (VNode | string)[] = [];
@@ -260,7 +264,7 @@ export default function(props: {
 					key: Math.random(),
 					url: token.props.url,
 					rel: 'nofollow noopener',
-				}, genEl(token.children, scale))];
+				}, genEl(token.children, scale, true))];
 			}
 
 			case 'mention': {
@@ -299,11 +303,11 @@ export default function(props: {
 				if (!props.nowrap) {
 					return [h('div', {
 						style: QUOTE_STYLE,
-					}, genEl(token.children, scale))];
+					}, genEl(token.children, scale, true))];
 				} else {
 					return [h('span', {
 						style: QUOTE_STYLE,
-					}, genEl(token.children, scale))];
+					}, genEl(token.children, scale, true))];
 				}
 			}
 
@@ -358,7 +362,7 @@ export default function(props: {
 			}
 
 			case 'plain': {
-				return [h('span', genEl(token.children, scale))];
+				return [h('span', genEl(token.children, scale, true))];
 			}
 
 			default: {
diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts
new file mode 100644
index 0000000000..0ac77e1006
--- /dev/null
+++ b/packages/frontend/src/scripts/nyaize.ts
@@ -0,0 +1,20 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function nyaize(text: string): string {
+	return text
+		// ja-JP
+		.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
+		// en-US
+		.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
+		.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
+		.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
+		// ko-KR
+		.replace(/[나-낳]/g, match => String.fromCharCode(
+			match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
+		))
+		.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
+		.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
+}
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 895f34689b..54bbfae145 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2977,6 +2977,8 @@ type UserLite = {
         faviconUrl: Instance['faviconUrl'];
         themeColor: Instance['themeColor'];
     };
+    isCat?: boolean;
+    isBot?: boolean;
 };
 
 // @public (undocumented)
@@ -2987,8 +2989,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
 // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
 // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
 // src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
-// src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
-// src/entities.ts:603:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
+// src/entities.ts:109:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
+// src/entities.ts:605:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
 // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index e05412abb9..659045bd6e 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -28,6 +28,8 @@ export type UserLite = {
 		faviconUrl: Instance['faviconUrl'];
 		themeColor: Instance['themeColor'];
 	};
+	isCat?: boolean;
+	isBot?: boolean;
 };
 
 export type UserDetailed = UserLite & {