diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index 85ad59f3a..4d1726421 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -18,7 +18,7 @@ import { KoaAdapter } from '@bull-board/koa';
 import { In, IsNull } from 'typeorm';
 import { fetchMeta } from '@/misc/fetch-meta.js';
 import config from '@/config/index.js';
-import { Users, Notes, UserProfiles, Pages, Channels, Clips } from '@/models/index.js';
+import { Users, Notes, UserProfiles, Pages, Channels, Clips, DriveFiles } from '@/models/index.js';
 import * as Acct from '@/misc/acct.js';
 import { getNoteSummary } from '@/misc/get-note-summary.js';
 import { queues } from '@/queue/queues.js';
@@ -327,10 +327,70 @@ router.get('/notes/:note', async (ctx, next) => {
 			const packedNote = await Notes.pack(note);
 			const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
 			const meta = await fetchMeta();
+
+			// If the note has a CW (is sensitive as a whole) or any of the files is sensitive or there are no
+			// files, they are not used for a preview.
+			let filesOpengraph = [];
+			if (!packedNote.cw || packedNote.files.length > 0 || packedNote.files.all(file => !file.isSensitive)) {
+				let limit = 4;
+				for (const file of packedNote.files) {
+					if (file.type.startsWith('image/')) {
+						filesOpengraph.push([
+							"og:image",
+							DriveFiles.getPublicUrl(file, true),
+						]);
+						filesOpengraph.push([
+							"og:image:type",
+							file.type,
+						]);
+						if (file.properties != null) {
+							filesOpengraph.push([
+								"og:image:width",
+								file.properties?.width,
+							]);
+							filesOpengraph.push([
+								"og:image:height",
+								file.properties?.height,
+							]);
+						}
+						if (file.comment) {
+							filesOpengraph.push([
+								"og:image:alt",
+								file.comment,
+							]);
+						}
+					} else if (file.type.startsWith('audio/')) {
+						filesOpengraph.push([
+							"og:audio",
+							DriveFiles.getPublicUrl(file),
+						]);
+						filesOpengraph.push([
+							"og:audio:type",
+							file.type,
+						]);
+					} else if (file.type.startsWith('video/')) {
+						filesOpengraph.push([
+							"og:video",
+							DriveFiles.getPublicUrl(file),
+						]);
+						filesOpengraph.push([
+							"og:video:type",
+							file.type,
+						]);
+					} else {
+						// doesn't count towards the limit
+						continue;
+					}
+
+					// limit the number of presented attachments
+					if (--limit < 0) break;
+				}
+			}
+
 			await ctx.render('note', {
 				note: packedNote,
 				profile,
-				avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })),
+				filesOpengraph,
 				// TODO: Let locale changeable by instance setting
 				summary: getNoteSummary(packedNote),
 				instanceName: meta.name || 'FoundKey',
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
index 37e8f8a41..0d9f50d89 100644
--- a/packages/backend/src/server/web/views/note.pug
+++ b/packages/backend/src/server/web/views/note.pug
@@ -17,7 +17,8 @@ block og
 	meta(property='og:title'       content= title)
 	meta(property='og:description' content= summary)
 	meta(property='og:url'         content= url)
-	meta(property='og:image'       content= avatarUrl)
+	for opengraphTag in filesOpengraph
+		meta(property=opengraphTag[0] content=opengraphTag[1])
 
 block meta
 	if user.host || isRenote || profile.noCrawle