diff --git a/packages/backend/src/remote/activitypub/renderer/block.ts b/packages/backend/src/remote/activitypub/renderer/block.ts
index 10a4fde5179718f6d95bea04c3ee0bbca207fe9c..13815fb76f856d8962570a7a5b28c6eaf222d166 100644
--- a/packages/backend/src/remote/activitypub/renderer/block.ts
+++ b/packages/backend/src/remote/activitypub/renderer/block.ts
@@ -1,8 +1,20 @@
 import config from '@/config/index.js';
-import { ILocalUser, IRemoteUser } from '@/models/entities/user.js';
+import { Blocking } from '@/models/entities/blocking.js';
 
-export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
-	type: 'Block',
-	actor: `${config.url}/users/${blocker.id}`,
-	object: blockee.uri,
-});
+/**
+ * Renders a block into its ActivityPub representation.
+ *
+ * @param block The block to be rendered. The blockee relation must be loaded.
+ */
+export function renderBlock(block: Blocking) {
+	if (block.blockee?.url == null) {
+		throw new Error('renderBlock: missing blockee uri');
+	}
+
+	return {
+		type: 'Block',
+		id: `${config.url}/blocks/${block.id}`,
+		actor: `${config.url}/users/${block.blockerId}`,
+		object: block.blockee.uri,
+	};
+}
diff --git a/packages/backend/src/remote/activitypub/renderer/follow.ts b/packages/backend/src/remote/activitypub/renderer/follow.ts
index 9e9692b77a03fc15415f362b77d78dceb8599c68..00fac18ad5b26b9c2df78b89081e26197e57ddab 100644
--- a/packages/backend/src/remote/activitypub/renderer/follow.ts
+++ b/packages/backend/src/remote/activitypub/renderer/follow.ts
@@ -4,12 +4,11 @@ import { Users } from '@/models/index.js';
 
 export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
 	const follow = {
+		id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
 		type: 'Follow',
 		actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
 		object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
 	} as any;
 
-	if (requestId) follow.id = requestId;
-
 	return follow;
 };
diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts
index a48c2d412254c2a622e131e66be9d8597c36d258..cd5f917c400e3caa875b729acb2d243f428d0653 100644
--- a/packages/backend/src/server/activitypub.ts
+++ b/packages/backend/src/server/activitypub.ts
@@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js';
 import { isSelfHost } from '@/misc/convert-host.js';
 import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
 import { ILocalUser, User } from '@/models/entities/user.js';
-import { In, IsNull } from 'typeorm';
+import { In, IsNull, Not } from 'typeorm';
 import { renderLike } from '@/remote/activitypub/renderer/like.js';
 import { getUserKeypair } from '@/misc/keypair-store.js';
+import renderFollow from '@/remote/activitypub/renderer/follow.js';
 
 // Init router
 const router = new Router();
@@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
 	setResponseType(ctx);
 });
 
+// follow
+router.get('/follows/:follower/:followee', async ctx => {
+	// This may be used before the follow is completed, so we do not
+	// check if the following exists.
+
+	const [follower, followee] = await Promise.all([
+		Users.findOneBy({
+			id: ctx.params.follower,
+			host: IsNull(),
+		}),
+		Users.findOneBy({
+			id: ctx.params.followee,
+			host: Not(IsNull()),
+		}),
+	]);
+
+	if (follower == null || followee == null) {
+		ctx.status = 404;
+		return;
+	}
+
+	ctx.body = renderActivity(renderFollow(follower, followee));
+	ctx.set('Cache-Control', 'public, max-age=180');
+	setResponseType(ctx);
+});
+
 export default router;
diff --git a/packages/backend/src/services/blocking/create.ts b/packages/backend/src/services/blocking/create.ts
index b2be78b220a23abadc6caeca6321364122fa8e6b..a2c61cca229f68b57ef3ca502a77103f4016a3f7 100644
--- a/packages/backend/src/services/blocking/create.ts
+++ b/packages/backend/src/services/blocking/create.ts
@@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js';
 import { renderActivity } from '@/remote/activitypub/renderer/index.js';
 import renderFollow from '@/remote/activitypub/renderer/follow.js';
 import renderUndo from '@/remote/activitypub/renderer/undo.js';
-import renderBlock from '@/remote/activitypub/renderer/block.js';
+import { renderBlock } from '@/remote/activitypub/renderer/block.js';
 import { deliver } from '@/queue/index.js';
 import renderReject from '@/remote/activitypub/renderer/reject.js';
+import { Blocking } from '@/models/entities/blocking.js';
 import { User } from '@/models/entities/user.js';
 import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
 import { perUserFollowingChart } from '@/services/chart/index.js';
@@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) {
 		removeFromList(blockee, blocker),
 	]);
 
-	await Blockings.insert({
+	const blocking = {
 		id: genId(),
 		createdAt: new Date(),
+		blocker,
 		blockerId: blocker.id,
+		blockee,
 		blockeeId: blockee.id,
-	});
+	} as Blocking;
+
+	await Blockings.insert(blocking);
 
 	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
-		const content = renderActivity(renderBlock(blocker, blockee));
+		const content = renderActivity(renderBlock(blocking));
 		deliver(blocker, content, blockee.inbox);
 	}
 }
diff --git a/packages/backend/src/services/blocking/delete.ts b/packages/backend/src/services/blocking/delete.ts
index d7b5ddd5ff76d5c492047c86af11aa795bdee54b..cb16651bc009e4956a9f37108ffda31d0b6a6138 100644
--- a/packages/backend/src/services/blocking/delete.ts
+++ b/packages/backend/src/services/blocking/delete.ts
@@ -1,5 +1,5 @@
 import { renderActivity } from '@/remote/activitypub/renderer/index.js';
-import renderBlock from '@/remote/activitypub/renderer/block.js';
+import { renderBlock } from '@/remote/activitypub/renderer/block.js';
 import renderUndo from '@/remote/activitypub/renderer/undo.js';
 import { deliver } from '@/queue/index.js';
 import Logger from '../logger.js';
@@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) {
 		return;
 	}
 
+	// Since we already have the blocker and blockee, we do not need to fetch
+	// them in the query above and can just manually insert them here.
+	blocking.blocker = blocker;
+	blocking.blockee = blockee;
+
 	Blockings.delete(blocking.id);
 
 	// deliver if remote bloking
 	if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
-		const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
+		const content = renderActivity(renderUndo(renderBlock(blocking), blocker));
 		deliver(blocker, content, blockee.inbox);
 	}
 }