diff --git a/docs/api-change.md b/docs/api-change.md
index f3ed584c32..dcd4329a27 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -2,6 +2,10 @@
 
 Breaking changes are indicated by the :warning: icon.
 
+## Unreleased
+
+- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional).
+
 ## v20240413
 
 - :warning: Removed `patrons` endpoint.
diff --git a/locales/en-US.yml b/locales/en-US.yml
index d552e1e3a2..f44c3d1842 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration"
 invite: "Invite"
 driveCapacityPerLocalAccount: "Drive capacity per local user"
 driveCapacityPerRemoteAccount: "Drive capacity per remote user"
+antennaLimit: "The maximum number of antennas that each user can create"
 inMb: "In megabytes"
 iconUrl: "Icon URL"
 bannerUrl: "Banner image URL"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index c496d05381..c8579a4c1a 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -340,6 +340,7 @@ invite: "邀请"
 driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
 driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
 inMb: "以兆字节 (MegaByte) 为单位"
+antennaLimit: "每个用户最多可以创建的天线数量"
 iconUrl: "图标 URL"
 bannerUrl: "横幅图 URL"
 backgroundImageUrl: "背景图 URL"
diff --git a/package.json b/package.json
index a62fae09f0..93594a5698 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,9 @@
 		"debug": "pnpm run build:debug && pnpm run start",
 		"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
 		"mocha": "pnpm --filter backend run mocha",
-		"test": "pnpm run mocha",
+		"test": "pnpm run test:ts && pnpm run test:rs",
+		"test:ts": "pnpm run mocha",
+		"test:rs": "cargo test",
 		"format": "pnpm run format:ts; pnpm run format:rs",
 		"format:ts": "pnpm -r --parallel run format",
 		"format:rs": "cargo fmt --all --",
diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 7fd93a3845..50489a846d 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -557,6 +557,7 @@ export interface Meta {
   recaptchaSecretKey: string | null
   localDriveCapacityMb: number
   remoteDriveCapacityMb: number
+  antennaLimit: number
   summalyProxy: string | null
   enableEmail: boolean
   email: string | null
diff --git a/packages/backend-rs/src/model/entity/meta.rs b/packages/backend-rs/src/model/entity/meta.rs
index a1e50228d5..3bf205d040 100644
--- a/packages/backend-rs/src/model/entity/meta.rs
+++ b/packages/backend-rs/src/model/entity/meta.rs
@@ -174,6 +174,8 @@ pub struct Model {
     pub more_urls: Json,
     #[sea_orm(column_name = "markLocalFilesNsfwByDefault")]
     pub mark_local_files_nsfw_by_default: bool,
+    #[sea_orm(column_name = "antennaLimit")]
+    pub antenna_limit: i32,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
diff --git a/packages/backend/src/migration/1712937600000-antennaLimit.ts b/packages/backend/src/migration/1712937600000-antennaLimit.ts
new file mode 100644
index 0000000000..cd8f9ff658
--- /dev/null
+++ b/packages/backend/src/migration/1712937600000-antennaLimit.ts
@@ -0,0 +1,19 @@
+import type { MigrationInterface, QueryRunner } from "typeorm";
+
+export class antennaLimit1712937600000 implements MigrationInterface {
+	async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(
+			`ALTER TABLE "meta" ADD "antennaLimit" integer NOT NULL DEFAULT 5`,
+			undefined,
+		);
+		await queryRunner.query(
+			`COMMENT ON COLUMN "meta"."antennaLimit" IS 'Antenna Limit'`,
+		);
+	}
+	async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(
+			`ALTER TABLE "meta" DROP COLUMN "antennaLimit"`,
+			undefined,
+		);
+	}
+}
diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts
index cdb8e14c3f..5e267a8e24 100644
--- a/packages/backend/src/models/entities/meta.ts
+++ b/packages/backend/src/models/entities/meta.ts
@@ -276,6 +276,12 @@ export class Meta {
 	})
 	public remoteDriveCapacityMb: number;
 
+	@Column("integer", {
+		default: 5,
+		comment: "Antenna Limit",
+	})
+	public antennaLimit: number;
+
 	@Column("varchar", {
 		length: 128,
 		nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index d0e639fcf1..ecfed950d3 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -24,6 +24,11 @@ export const meta = {
 				optional: false,
 				nullable: false,
 			},
+			antennaLimit: {
+				type: "number",
+				optional: false,
+				nullable: false,
+			},
 			cacheRemoteFiles: {
 				type: "boolean",
 				optional: false,
@@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => {
 		enableGuestTimeline: instance.enableGuestTimeline,
 		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
 		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
+		antennaLimit: instance.antennaLimit,
 		emailRequiredForSignup: instance.emailRequiredForSignup,
 		enableHcaptcha: instance.enableHcaptcha,
 		hcaptchaSiteKey: instance.hcaptchaSiteKey,
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 604ef3a0fc..e5234ea720 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -94,6 +94,7 @@ export const paramDef = {
 		defaultDarkTheme: { type: "string", nullable: true },
 		localDriveCapacityMb: { type: "integer" },
 		remoteDriveCapacityMb: { type: "integer" },
+		antennaLimit: { type: "integer" },
 		cacheRemoteFiles: { type: "boolean" },
 		markLocalFilesNsfwByDefault: { type: "boolean" },
 		emailRequiredForSignup: { type: "boolean" },
@@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => {
 		set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
 	}
 
+	if (ps.antennaLimit !== undefined) {
+		set.antennaLimit = ps.antennaLimit;
+	}
+
 	if (ps.cacheRemoteFiles !== undefined) {
 		set.cacheRemoteFiles = ps.cacheRemoteFiles;
 	}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 792301d4de..aa5dcee044 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -1,5 +1,5 @@
 import define from "@/server/api/define.js";
-import { genId } from "backend-rs";
+import { fetchMeta, genId } from "backend-rs";
 import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
 import { ApiError } from "@/server/api/error.js";
 import { publishInternalEvent } from "@/services/stream.js";
@@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => {
 	let userList;
 	let userGroupJoining;
 
+	const instance = await fetchMeta(true);
+
 	const antennas = await Antennas.findBy({
 		userId: user.id,
 	});
-	if (antennas.length > 5 && !user.isAdmin) {
+	if (antennas.length >= instance.antennaLimit) {
 		throw new ApiError(meta.errors.tooManyAntennas);
 	}
 
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 0167377944..f35ae9cc6b 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -126,6 +126,11 @@ export const meta = {
 				optional: false,
 				nullable: false,
 			},
+			antennaLimit: {
+				type: "number",
+				optional: false,
+				nullable: false,
+			},
 			cacheRemoteFiles: {
 				type: "boolean",
 				optional: false,
@@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => {
 		enableGuestTimeline: instance.enableGuestTimeline,
 		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
 		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
+		antennaLimit: instance.antennaLimit,
 		emailRequiredForSignup: instance.emailRequiredForSignup,
 		enableHcaptcha: instance.enableHcaptcha,
 		hcaptchaSiteKey: instance.hcaptchaSiteKey,
diff --git a/packages/backend/src/server/web/feed.ts b/packages/backend/src/server/web/feed.ts
index 5208ee70e4..3beffc82f0 100644
--- a/packages/backend/src/server/web/feed.ts
+++ b/packages/backend/src/server/web/feed.ts
@@ -2,7 +2,16 @@ import { Feed } from "feed";
 import { In, IsNull } from "typeorm";
 import { config } from "@/config.js";
 import type { User } from "@/models/entities/user.js";
+import type { Note } from "@/models/entities/note.js";
 import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js";
+import getNoteHtml from "@/remote/activitypub/misc/get-note-html.js";
+
+/**
+ * If there is this part in the note, it will cause CDATA to be terminated early.
+ */
+function escapeCDATA(str: string) {
+	return str.replaceAll("]]>", "]]]]><![CDATA[>");
+}
 
 export default async function (
 	user: User,
@@ -15,7 +24,7 @@ export default async function (
 	const author = {
 		link: `${config.url}/@${user.username}`,
 		email: `${user.username}@${config.host}`,
-		name: user.name || user.username,
+		name: escapeCDATA(user.name || user.username),
 	};
 
 	const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
@@ -44,11 +53,13 @@ export default async function (
 		title: `${author.name} (@${user.username}@${config.host})`,
 		updated: notes[0].createdAt,
 		generator: "Firefish",
-		description: `${user.notesCount} Notes, ${
-			profile.ffVisibility === "public" ? user.followingCount : "?"
-		} Following, ${
-			profile.ffVisibility === "public" ? user.followersCount : "?"
-		} Followers${profile.description ? ` · ${profile.description}` : ""}`,
+		description: escapeCDATA(
+			`${user.notesCount} Notes, ${
+				profile.ffVisibility === "public" ? user.followingCount : "?"
+			} Following, ${
+				profile.ffVisibility === "public" ? user.followersCount : "?"
+			} Followers${profile.description ? ` · ${profile.description}` : ""}`,
+		),
 		link: author.link,
 		image: await Users.getAvatarUrl(user),
 		feedLinks: {
@@ -88,19 +99,23 @@ export default async function (
 		}
 
 		feed.addItem({
-			title: title
-				.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
-				.substring(0, 100),
+			title: escapeCDATA(
+				title
+					.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
+					.substring(0, 100),
+			),
 			link: `${config.url}/notes/${note.id}`,
 			date: note.createdAt,
 			description: note.cw
-				? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
+				? escapeCDATA(note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""))
 				: undefined,
-			content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
+			content: escapeCDATA(
+				contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
+			),
 		});
 	}
 
-	async function noteToString(note, isTheNote = false) {
+	async function noteToString(note: Note, isTheNote = false) {
 		const author = isTheNote
 			? null
 			: await Users.findOneBy({ id: note.userId });
@@ -135,7 +150,10 @@ export default async function (
 				}">${file.name}</a>`;
 			}
 		}
-		outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
+
+		outstr += `${note.cw ? note.cw + "<br>" : ""}${
+			getNoteHtml(note) || ""
+		}${fileEle}`;
 		if (isTheNote) {
 			outstr += ` <span class="${
 				note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index f69c4fb23b..1c2188daac 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -350,6 +350,19 @@
 							</FormSplit>
 						</FormSection>
 
+						<FormSection>
+							<template #label>{{ i18n.ts.antennas }}</template>
+							<FormInput
+									v-model="antennaLimit"
+									type="number"
+									class="_formBlock"
+								>
+									<template #label>{{
+										i18n.ts.antennaLimit
+									}}</template>
+								</FormInput>
+						</FormSection>
+
 						<FormSection>
 							<template #label>ServiceWorker</template>
 
@@ -502,6 +515,7 @@ const cacheRemoteFiles = ref(false);
 const markLocalFilesNsfwByDefault = ref(false);
 const localDriveCapacityMb = ref(0);
 const remoteDriveCapacityMb = ref(0);
+const antennaLimit = ref(0);
 const enableRegistration = ref(false);
 const emailRequiredForSignup = ref(false);
 const enableServiceWorker = ref(false);
@@ -579,6 +593,7 @@ async function init() {
 	markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault;
 	localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb;
 	remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb;
+	antennaLimit.value = meta.antennaLimit;
 	enableRegistration.value = !meta.disableRegistration;
 	emailRequiredForSignup.value = meta.emailRequiredForSignup;
 	enableServiceWorker.value = meta.enableServiceWorker;
@@ -631,6 +646,7 @@ function save() {
 		markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value,
 		localDriveCapacityMb: localDriveCapacityMb.value,
 		remoteDriveCapacityMb: remoteDriveCapacityMb.value,
+		antennaLimit: antennaLimit.value,
 		disableRegistration: !enableRegistration.value,
 		emailRequiredForSignup: emailRequiredForSignup.value,
 		enableServiceWorker: enableServiceWorker.value,
diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue
index d0c93899aa..8c97448f72 100644
--- a/packages/client/src/pages/note-history.vue
+++ b/packages/client/src/pages/note-history.vue
@@ -4,30 +4,35 @@
 			><MkPageHeader :display-back-button="true"
 		/></template>
 		<MkSpacer :content-max="800">
-			<MkLoading v-if="!loaded" />
-			<MkPagination
-				v-else
-				ref="pagingComponent"
-				v-slot="{ items }"
-				:pagination="pagination"
-			>
-				<div ref="tlEl" class="giivymft noGap">
-					<XList
-						v-slot="{ item }"
-						:items="convertNoteEditsToNotes(items)"
-						class="notes"
-						:no-gap="true"
-					>
-						<XNote
-							:key="item.id"
-							class="qtqtichx"
-							:note="item"
-							:hide-footer="true"
-							:detailed-view="true"
-						/>
-					</XList>
-				</div>
-			</MkPagination>
+			<MkLoading v-if="note == null" />
+			<div v-else>
+				<MkRemoteCaution
+					v-if="note.user.host != null"
+					:href="note.url ?? note.uri!"
+				/>
+				<MkPagination
+					ref="pagingComponent"
+					v-slot="{ items }"
+					:pagination="pagination"
+				>
+					<div ref="tlEl" class="giivymft noGap">
+						<XList
+							v-slot="{ item }"
+							:items="convertNoteEditsToNotes(items)"
+							class="notes"
+							:no-gap="true"
+						>
+							<XNote
+								:key="item.id"
+								class="qtqtichx"
+								:note="item"
+								:hide-footer="true"
+								:detailed-view="true"
+							/>
+						</XList>
+					</div>
+				</MkPagination>
+			</div>
 		</MkSpacer>
 	</MkStickyContainer>
 </template>
@@ -44,6 +49,7 @@ import XNote from "@/components/MkNote.vue";
 import { i18n } from "@/i18n";
 import { definePageMetadata } from "@/scripts/page-metadata";
 import icon from "@/scripts/icon";
+import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
 
 const pagingComponent = ref<MkPaginationType<
 	typeof pagination.endpoint
@@ -69,8 +75,7 @@ definePageMetadata(
 	})),
 );
 
-const note = ref<entities.Note>({} as entities.Note);
-const loaded = ref(false);
+const note = ref<entities.Note | null>(null);
 
 onMounted(() => {
 	api("notes/show", {
@@ -83,20 +88,19 @@ onMounted(() => {
 		res.replyId = null;
 
 		note.value = res;
-		loaded.value = true;
 	});
 });
 
 function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
 	const now: entities.NoteEdit = {
 		id: "EditionNow",
-		noteId: note.value.id,
-		updatedAt: note.value.createdAt,
-		text: note.value.text,
-		cw: note.value.cw,
-		files: note.value.files,
-		fileIds: note.value.fileIds,
-		emojis: note.value.emojis,
+		noteId: note.value!.id,
+		updatedAt: note.value!.createdAt,
+		text: note.value!.text,
+		cw: note.value!.cw,
+		files: note.value!.files,
+		fileIds: note.value!.fileIds,
+		emojis: note.value!.emojis,
 	};
 
 	return [now]
@@ -112,7 +116,7 @@ function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
 				_shouldInsertAd_: false,
 				files: noteEdit.files,
 				fileIds: noteEdit.fileIds,
-				emojis: note.value.emojis.concat(noteEdit.emojis),
+				emojis: note.value!.emojis.concat(noteEdit.emojis),
 			});
 		});
 }
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 457d7ac935..67fb41988d 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -356,6 +356,7 @@ export type LiteInstanceMetadata = {
 	disableGlobalTimeline: boolean;
 	driveCapacityPerLocalUserMb: number;
 	driveCapacityPerRemoteUserMb: number;
+	antennaLimit: number;
 	enableHcaptcha: boolean;
 	hcaptchaSiteKey: string | null;
 	enableRecaptcha: boolean;