diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 8200b24fd..417f50f95 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -75,7 +75,7 @@ export class ServerService {
 		fastify.register(this.nodeinfoServerService.createServer);
 		fastify.register(this.wellKnownServerService.createServer);
 
-		fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
+		fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
 			const path = request.params.path;
 
 			reply.header('Cache-Control', 'public, max-age=86400');
@@ -105,11 +105,19 @@ export class ServerService {
 				}
 			}
 
-			const url = new URL(`${this.config.mediaProxy}/emoji.webp`);
-			// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
-			url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
-			url.searchParams.set('emoji', '1');
-			if ('static' in request.query) url.searchParams.set('static', '1');
+			let url: URL;
+			if ('badge' in request.query) {
+				url = new URL(`${this.config.mediaProxy}/emoji.png`);
+				// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
+				url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
+				url.searchParams.set('badge', '1');
+			} else {
+				url = new URL(`${this.config.mediaProxy}/emoji.webp`);
+				// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
+				url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
+				url.searchParams.set('emoji', '1');
+				if ('static' in request.query) url.searchParams.set('static', '1');
+			}
 
 			return await reply.redirect(
 				301,
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index da92b37d1..6286d076c 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -138,26 +138,11 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
 
 					if (reaction.startsWith(':')) {
 						// カスタム絵文字の場合
-						const customEmoji = data.body.note.emojis.find(x => x.name === reaction.substr(1, reaction.length - 2));
-						if (customEmoji) {
-							if (reaction.includes('@')) {
-								reaction = `:${reaction.substr(1, reaction.indexOf('@') - 1)}:`;
-							}
-
-							const u = new URL(customEmoji.url);
-							if (u.href.startsWith(`${origin}/proxy/`)) {
-								// もう既にproxyっぽそうだったらsearchParams付けるだけ
-								u.searchParams.set('badge', '1');
-								badge = u.href;
-							} else {
-								// 拡張子がないとキャッシュしてくれないCDNがあるので
-								const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.png`;
-								badge = `${origin}/proxy/${dummy}?${url.query({
-									url: u.href,
-									badge: '1',
-								})}`;
-							}
-						}
+						const name = reaction.substring(1, reaction.length - 1);
+						badge = `${origin}/emoji/${name}.webp?${url.query({
+							badge: '1',
+						})}`;
+						reaction = name.split('@')[0];
 					} else {
 						// Unicode絵文字の場合
 						badge = `/twemoji-badge/${char2fileName(reaction)}.png`;
@@ -171,6 +156,7 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
 					return [`${reaction} ${getUserName(data.body.user)}`, {
 						body: data.body.note.text ?? '',
 						icon: data.body.user.avatarUrl,
+						tag,
 						badge,
 						data,
 						actions: [