From 2cbe1d1210a5745787f37069ecb59b8f6c03c224 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 9 Sep 2024 20:57:36 +0900
Subject: feat(frontend): ノート・ユーザータイムライン埋め込み (#13929)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix

* navhookをbootに移動

* サーバーサイドのbootも分けるように

* 埋め込みページかどうかの判定は最初の一回だけに

* tooltipは出せるように

* fix design

* 埋め込み独自のtooltipを削除

* ロジックの分岐が多かったMkNoteDetailedを分離

* fix indent

* プレビュー用iframeにフォーカスが当たるのを修正

* popupの制御を出す側で行うように

* パラメータが逆になっていたのを修正

* Update MkEmbedCodeGenDialog.vue

* fix

* eliminate misskey-js lint warns

* fix

* add appropriate attributes to embed html

* enhance: サーバーサイドのembed系をさらに分離

* enhance: embed routerを分離（route定義をboot時に変更できるようにする改修を含む）

* type

* lint

* fix indent

* server-side styleを完全に分離

* Revert "refactor: 画面サイズのしきい値をconstにまとめる"

This reverts commit 05ca36f400889456981e89489ae0ae242fa09b67.

* fix

* revert all changes in base.pug

* embedドメインをまとめた

* embedドメインをまとめた

* prevent calling contextmenu in embed page by stopping at the caller

* fix import

* fix import

* improve directory structure

* fix import

* register timeline ui as a container

* wa-

* rename

* wa-

* Update EmMediaList.vue

* Update EmMediaList.vue

* Update EmMediaList.vue

* Update EmMediaImage.vue

* Update EmNote.vue

* revert mkmedialist changes

* 戻し漏れ

* wip

* tweak embed media ui

* revert original media components

* Update boot.embed.js

* rename

* wip

* Update MkNote.vue

* wip

* Update MkSubNoteContent.vue

* Update EmNote.vue

* Update packages/frontend/src/router/definition.ts

* Revert "Update packages/frontend/src/router/definition.ts"

This reverts commit 937ae44521cdb0f250796943b20142b65f8ed944.

* refactor EmMediaImage

* fix import

* remove unused imports

* Update router.ts

* wip

* Update boot.ts

* wip

* wip

* wip

* wip

* Update EmNote.vue

* Update EmNote.vue

* Create EmA.vue

* Create EmAvatar.vue

* Update EmAvatar.vue

* wip

* wip

* wip

* Create EmImgWithBlurhash.vue

* Update EmImgWithBlurhash.vue

* Create EmPagination.vue

* wip

* Update boot.ts

* wip

* wip

* wi@p

* wip

* wip

* wiop

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update boot.ts

* wip

* Update MkMisskeyFlavoredMarkdown.ts

* wip

* wip

* wip

* wip

* wip

* Update post-message.ts

* wip

* Update EmNoteDetailed.vue

* Update EmNoteDetailed.vue

* Create instance.ts

* Update EmNoteDetailed.vue

* wip

* Update EmNoteDetailed.vue

* wip

* wip

* wip

* Update pnpm-lock.yaml

* wip

* wip

* wp

* wip

* Update ClientServerService.ts

* wip

* Update boot.ts

* Update vite.config.local-dev.ts

* Update vite.config.ts

* Create index.html

* wa-

* wip

* Update boot.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Create EmLink.vue

* Create EmMention.vue

* Update EmMfm.ts

* wip

* wip

* wip

* wip

* Update vite.config.ts

* Update boot.ts

* Update EmA.vue

* うぃｐ

* wip

* wip

* Create EmError.vue

* wip

* Update MkEmbedCodeGenDialog.vue

* Update EmNote.vue

* wip

* wip

* Update user-timeline.vue

* Update check-spdx-license-id.yml

* wip

* wip

* style(frontend-shared): lint fixes on build.js

* fix(frontend-shared): include `*.{js,json}` files in js-built

* wip

* use alias

* refactor

* refactor

* Update scroll.ts

* refactor

* refactor

* refactor

* wip

* wip

* wip

* wip

* Update roles.vue

* Update branding.vue

* wip

* wip

* wip

* Update page.vue

* wip

* fix import

* add missing css variables

* 絵文字をtwemojiに変更

クライアントデフォルトにあわせるため

* force empoll readonly

* fix compiler error

* fix broken imports

* tweak button style

* run api extractor

* fix storybook theme preloads

* fix storybook instance imports

* Update preview.ts

* Update preview.ts

* Update preview.ts

* Revert "Update preview.ts"

This reverts commit 12bab1c6fbd3baf753515df760ff19d027b85155.

* Revert "Update preview.ts"

This reverts commit 5c0ce01dbdf2194ffe94aba950f747a9968f29c4.

* Revert "Update preview.ts"

This reverts commit f4863524d7e5ca0f25470808849c24a72bea000a.

* Revert "fix storybook instance imports"

This reverts commit ed8eabb246edf731d31adffbe3c77c539e53ae9e.

* Revert "wip"

This reverts commit d3c1926519878155193a1654f49141e515d49683.

* Revert "Update page.vue"

This reverts commit 27c7900b0c1ae296b56075e8a9c22585d9cd744b.

* Revert "Update branding.vue"

This reverts commit c08ccb65ba66774c3e2b3dcfc6153004b5c0aa16.

* Revert "Update roles.vue"

This reverts commit 1488b670660cb1803d17d8f5c78f2d79e59fa52d.

* Revert "wip"

This reverts commit aab1c769814b08c257cad3025422a0eea3bfba4f.

* refactor: use common media proxy

* fix imports

* fix

* fix: MediaProxyの初期化を保証する（storybook対策？）

* enhance(frontend-embed): improve embedParams provide

* fix(backend): MK_DEV_PREFER=backendのときにembed viteが読み込めないのを修正

* fix

* embed-pageを共通化

* fix import

* fix import

* fix import

* const.jsを共通化

（たぶんrevertしすぎた）

* fix type error

* fix duplicated import

* fix lint

* fix

* コメントとして残す

* sharedとembedをlint対象にする

* lint

* attempt to fix eslint (frontend-shared)

* lint fixes

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com>
---
 packages/frontend-embed/src/components/EmNotes.vue | 48 ++++++++++++++++++++++
 1 file changed, 48 insertions(+)
 create mode 100644 packages/frontend-embed/src/components/EmNotes.vue

(limited to 'packages/frontend-embed/src/components/EmNotes.vue')

diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
new file mode 100644
index 0000000000..3970d05098
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -0,0 +1,48 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<EmPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
+	<template #empty>
+		<div class="_fullinfo">
+			<div>{{ i18n.ts.noNotes }}</div>
+		</div>
+	</template>
+
+	<template #default="{ items: notes }">
+		<div :class="[$style.root]">
+			<EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
+		</div>
+	</template>
+</EmPagination>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef } from 'vue';
+import EmNote from '@/components/EmNote.vue';
+import EmPagination, { Paging } from '@/components/EmPagination.vue';
+import { i18n } from '@/i18n.js';
+
+const props = withDefaults(defineProps<{
+	pagination: Paging;
+	noGap?: boolean;
+	disableAutoLoad?: boolean;
+	ad?: boolean;
+}>(), {
+	ad: true,
+});
+
+const pagingComponent = shallowRef<InstanceType<typeof EmPagination>>();
+
+defineExpose({
+	pagingComponent,
+});
+</script>
+
+<style lang="scss" module>
+.root {
+	background: var(--panel);
+}
+</style>
-- 
cgit v1.3.1-freya


From 672779a15f27ab8ad35ee5e6e0f7205497e02151 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Mon, 9 Sep 2024 22:44:39 +0900
Subject: fix(frontend-embed): 不足していたスタイル・インポートを追加 (#14531)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend-embed): add missing imports

* fix(frontend-embed): add missing styles
---
 packages/frontend-embed/src/components/EmNote.vue           | 2 +-
 packages/frontend-embed/src/components/EmNoteSimple.vue     | 1 +
 packages/frontend-embed/src/components/EmNoteSub.vue        | 2 ++
 packages/frontend-embed/src/components/EmNotes.vue          | 4 ++++
 packages/frontend-embed/src/components/EmSubNoteContent.vue | 1 +
 packages/frontend-embed/src/pages/clip.vue                  | 3 ++-
 6 files changed, 11 insertions(+), 2 deletions(-)

(limited to 'packages/frontend-embed/src/components/EmNotes.vue')

diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index 7c4d591066..dce997f0ef 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -104,7 +104,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
+import { computed, inject, ref, shallowRef } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import I18n from '@/components/I18n.vue';
diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue
index 828b6cd2e2..704a876e59 100644
--- a/packages/frontend-embed/src/components/EmNoteSimple.vue
+++ b/packages/frontend-embed/src/components/EmNoteSimple.vue
@@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
+import EmAvatar from '@/components/EmAvatar.vue';
 import EmNoteHeader from '@/components/EmNoteHeader.vue';
 import EmSubNoteContent from '@/components/EmSubNoteContent.vue';
 import EmMfm from '@/components/EmMfm.js';
diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue
index c98b956805..f60aea3e7e 100644
--- a/packages/frontend-embed/src/components/EmNoteSub.vue
+++ b/packages/frontend-embed/src/components/EmNoteSub.vue
@@ -33,6 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import EmA from '@/components/EmA.vue';
+import EmAvatar from '@/components/EmAvatar.vue';
 import EmNoteHeader from '@/components/EmNoteHeader.vue';
 import EmSubNoteContent from '@/components/EmSubNoteContent.vue';
 import { notePage } from '@/utils.js';
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
index 3970d05098..6370f4aeae 100644
--- a/packages/frontend-embed/src/components/EmNotes.vue
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -45,4 +45,8 @@ defineExpose({
 .root {
 	background: var(--panel);
 }
+
+.note {
+	border-bottom: 0.5px solid var(--divider);
+}
 </style>
diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue
index 382e39e492..f7d653ab3f 100644
--- a/packages/frontend-embed/src/components/EmSubNoteContent.vue
+++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue
@@ -37,6 +37,7 @@ import EmPoll from '@/components/EmPoll.vue';
 import { i18n } from '@/i18n.js';
 import { url } from '@/config.js';
 import { shouldCollapsed } from '@/to-be-shared/collapsed.js';
+import EmA from '@/components/EmA.vue';
 import EmMfm from '@/components/EmMfm.js';
 
 const props = defineProps<{
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
index 6564eecd75..28bd0ab772 100644
--- a/packages/frontend-embed/src/pages/clip.vue
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<MkLoading v-if="loading"/>
+	<EmLoading v-if="loading"/>
 	<EmTimelineContainer v-else-if="clip" :showHeader="embedParams.header">
 		<template #header>
 			<div :class="$style.clipHeader">
@@ -43,6 +43,7 @@ import { ref, computed, shallowRef, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import { scrollToTop } from '@@/js/scroll.js';
 import type { Paging } from '@/components/EmPagination.vue';
+import EmLoading from '@/components/EmLoading.vue';
 import EmNotes from '@/components/EmNotes.vue';
 import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
-- 
cgit v1.3.1-freya


From 3f0aaaa41efe42776d70490ea213e3c8b194c152 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Mon, 23 Sep 2024 19:49:52 +0900
Subject: perf(embed): improve embed performance (#14613)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* wip

* refactor

* refactor

---------

Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
---
 .../backend/src/server/web/ClientServerService.ts  | 66 ++++++++++++++++++++++
 .../backend/src/server/web/views/base-embed.pug    |  3 +
 packages/frontend-embed/src/boot.ts                | 10 +++-
 .../src/components/EmNoteDetailed.vue              |  4 +-
 packages/frontend-embed/src/components/EmNotes.vue |  6 +-
 packages/frontend-embed/src/di.ts                  |  2 +
 packages/frontend-embed/src/pages/clip.vue         | 44 ++++++++-------
 packages/frontend-embed/src/pages/note.vue         | 33 +++++------
 packages/frontend-embed/src/pages/tag.vue          |  7 ++-
 .../frontend-embed/src/pages/user-timeline.vue     | 51 ++++++++++-------
 packages/frontend-embed/src/server-context.ts      | 21 +++++++
 packages/frontend-embed/src/ui.vue                 | 16 ++++--
 12 files changed, 190 insertions(+), 73 deletions(-)
 create mode 100644 packages/frontend-embed/src/server-context.ts

(limited to 'packages/frontend-embed/src/components/EmNotes.vue')

diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 063141273a..5de1f87667 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -785,6 +785,72 @@ export class ClientServerService {
 		//#endregion
 
 		//#region embed pages
+		fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			const user = await this.usersRepository.findOneBy({
+				id: request.params.user,
+			});
+
+			if (user == null) return;
+			if (user.host != null) return;
+
+			const _user = await this.userEntityService.pack(user);
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+				embedCtx: htmlSafeJsonStringify({
+					user: _user,
+				}),
+			});
+		});
+
+		fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			const note = await this.notesRepository.findOneBy({
+				id: request.params.note,
+			});
+
+			if (note == null) return;
+			if (note.visibility !== 'public') return;
+			if (note.userHost != null) return;
+
+			const _note = await this.noteEntityService.pack(note, null, { detail: true });
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+				embedCtx: htmlSafeJsonStringify({
+					note: _note,
+				}),
+			});
+		});
+
+		fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
+			reply.removeHeader('X-Frame-Options');
+
+			const clip = await this.clipsRepository.findOneBy({
+				id: request.params.clip,
+			});
+
+			if (clip == null) return;
+
+			const _clip = await this.clipEntityService.pack(clip);
+
+			reply.header('Cache-Control', 'public, max-age=3600');
+			return await reply.view('base-embed', {
+				title: this.meta.name ?? 'Misskey',
+				...await this.generateCommonPugData(this.meta),
+				embedCtx: htmlSafeJsonStringify({
+					clip: _clip,
+				}),
+			});
+		});
+
 		fastify.get('/embed/*', async (request, reply) => {
 			reply.removeHeader('X-Frame-Options');
 
diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug
index d773f2676a..2bab20a36c 100644
--- a/packages/backend/src/server/web/views/base-embed.pug
+++ b/packages/backend/src/server/web/views/base-embed.pug
@@ -43,6 +43,9 @@ html(class='embed')
 		script(type='application/json' id='misskey_meta' data-generated-at=now)
 			!= metaJson
 
+		script(type='application/json' id='misskey_embedCtx' data-generated-at=now)
+			!= embedCtx
+
 		script
 			include ../boot.embed.js
 
diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts
index fcea7d32ea..00c7944eb3 100644
--- a/packages/frontend-embed/src/boot.ts
+++ b/packages/frontend-embed/src/boot.ts
@@ -20,16 +20,19 @@ import { serverMetadata } from '@/server-metadata.js';
 import { url } from '@@/js/config.js';
 import { parseEmbedParams } from '@@/js/embed-page.js';
 import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
+import { serverContext } from '@/server-context.js';
 
 import type { Theme } from '@/theme.js';
 
 console.log('Misskey Embed');
 
+//#region Embedパラメータの取得・パース
 const params = new URLSearchParams(location.search);
 const embedParams = parseEmbedParams(params);
-
 if (_DEV_) console.log(embedParams);
+//#endregion
 
+//#region テーマ
 function parseThemeOrNull(theme: string | null): Theme | null {
 	if (theme == null) return null;
 	try {
@@ -65,6 +68,7 @@ if (embedParams.colorMode === 'dark') {
 		}
 	});
 }
+//#endregion
 
 // サイズの制限
 document.documentElement.style.maxWidth = '500px';
@@ -89,6 +93,10 @@ const app = createApp(
 
 app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url));
 
+app.provide(DI.serverMetadata, serverMetadata);
+
+app.provide(DI.serverContext, serverContext);
+
 app.provide(DI.embedParams, embedParams);
 
 // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
index 8169f500a9..a233011af7 100644
--- a/packages/frontend-embed/src/components/EmNoteDetailed.vue
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -142,8 +142,8 @@ import EmAcct from '@/components/EmAcct.vue';
 import { userPage } from '@/utils.js';
 import { notePage } from '@/utils.js';
 import { i18n } from '@/i18n.js';
+import { DI } from '@/di.js';
 import { shouldCollapsed } from '@@/js/collapsed.js';
-import { serverMetadata } from '@/server-metadata.js';
 import { url } from '@@/js/config.js';
 import EmMfm from '@/components/EmMfm.js';
 
@@ -151,6 +151,8 @@ const props = defineProps<{
 	note: Misskey.entities.Note;
 }>();
 
+const serverMetadata = inject(DI.serverMetadata)!;
+
 const inChannel = inject('inChannel', null);
 
 const note = ref(props.note);
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
index 6370f4aeae..3418d97f77 100644
--- a/packages/frontend-embed/src/components/EmNotes.vue
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { shallowRef } from 'vue';
+import { useTemplateRef } from 'vue';
 import EmNote from '@/components/EmNote.vue';
 import EmPagination, { Paging } from '@/components/EmPagination.vue';
 import { i18n } from '@/i18n.js';
 
-const props = withDefaults(defineProps<{
+withDefaults(defineProps<{
 	pagination: Paging;
 	noGap?: boolean;
 	disableAutoLoad?: boolean;
@@ -34,7 +34,7 @@ const props = withDefaults(defineProps<{
 	ad: true,
 });
 
-const pagingComponent = shallowRef<InstanceType<typeof EmPagination>>();
+const pagingComponent = useTemplateRef('pagingComponent');
 
 defineExpose({
 	pagingComponent,
diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts
index 799bbed598..22f6276630 100644
--- a/packages/frontend-embed/src/di.ts
+++ b/packages/frontend-embed/src/di.ts
@@ -7,9 +7,11 @@ import type { InjectionKey } from 'vue';
 import * as Misskey from 'misskey-js';
 import { MediaProxy } from '@@/js/media-proxy.js';
 import type { ParsedEmbedParams } from '@@/js/embed-page.js';
+import type { ServerContext } from '@/server-context.js';
 
 export const DI = {
 	serverMetadata: Symbol() as InjectionKey<Misskey.entities.MetaDetailed>,
 	embedParams: Symbol() as InjectionKey<ParsedEmbedParams>,
+	serverContext: Symbol() as InjectionKey<ServerContext>,
 	mediaProxy: Symbol() as InjectionKey<MediaProxy>,
 };
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
index 957d425d93..2528dc4b80 100644
--- a/packages/frontend-embed/src/pages/clip.vue
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -5,8 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<EmLoading v-if="loading"/>
-	<EmTimelineContainer v-else-if="clip" :showHeader="embedParams.header">
+	<EmTimelineContainer v-if="clip" :showHeader="embedParams.header">
 		<template #header>
 			<div :class="$style.clipHeader">
 				<div :class="$style.headerClipIconRoot">
@@ -39,20 +38,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { ref, computed, shallowRef, inject } from 'vue';
+import { ref, computed, inject, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { scrollToTop } from '@@/js/scroll.js';
+import { url, instanceName } from '@@/js/config.js';
+import { isLink } from '@@/js/is-link.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
 import type { Paging } from '@/components/EmPagination.vue';
-import EmLoading from '@/components/EmLoading.vue';
 import EmNotes from '@/components/EmNotes.vue';
 import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
 import { misskeyApi } from '@/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { serverMetadata } from '@/server-metadata.js';
-import { url, instanceName } from '@@/js/config.js';
-import { isLink } from '@@/js/is-link.js';
-import { defaultEmbedParams } from '@@/js/embed-page.js';
+import { assertServerContext } from '@/server-context.js';
 import { DI } from '@/di.js';
 
 const props = defineProps<{
@@ -61,16 +59,30 @@ const props = defineProps<{
 
 const embedParams = inject(DI.embedParams, defaultEmbedParams);
 
-const clip = ref<Misskey.entities.Clip | null>(null);
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const serverContext = inject(DI.serverContext)!;
+
+const clip = ref<Misskey.entities.Clip | null>();
+
+if (assertServerContext(serverContext, 'clip')) {
+	clip.value = serverContext.clip;
+} else {
+	clip.value = await misskeyApi('clips/show', {
+		clipId: props.clipId,
+	}).catch(() => {
+		return null;
+	});
+}
+
 const pagination = computed(() => ({
 	endpoint: 'clips/notes',
 	params: {
 		clipId: props.clipId,
 	},
 } as Paging));
-const loading = ref(true);
 
-const notesEl = shallowRef<InstanceType<typeof EmNotes> | null>(null);
+const notesEl = useTemplateRef('notesEl');
 
 function top(ev: MouseEvent) {
 	const target = ev.target as HTMLElement | null;
@@ -80,16 +92,6 @@ function top(ev: MouseEvent) {
 		scrollToTop(notesEl.value.$el as HTMLElement, { behavior: 'smooth' });
 	}
 }
-
-misskeyApi('clips/show', {
-	clipId: props.clipId,
-}).then(res => {
-	clip.value = res;
-	loading.value = false;
-}).catch(err => {
-	console.error(err);
-	loading.value = false;
-});
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue
index 86aebe072a..918583ecc7 100644
--- a/packages/frontend-embed/src/pages/note.vue
+++ b/packages/frontend-embed/src/pages/note.vue
@@ -5,40 +5,37 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.noteEmbedRoot">
-	<EmLoading v-if="loading"/>
-	<EmNoteDetailed v-else-if="note" :note="note"/>
+	<EmNoteDetailed v-if="note" :note="note"/>
 	<XNotFound v-else/>
 </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
+import { inject, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import EmNoteDetailed from '@/components/EmNoteDetailed.vue';
-import EmLoading from '@/components/EmLoading.vue';
 import XNotFound from '@/pages/not-found.vue';
+import { DI } from '@/di.js';
 import { misskeyApi } from '@/misskey-api.js';
+import { assertServerContext } from '@/server-context';
 
 const props = defineProps<{
 	noteId: string;
 }>();
 
+const serverContext = inject(DI.serverContext)!;
+
 const note = ref<Misskey.entities.Note | null>(null);
-const loading = ref(true);
 
-// TODO: クライアント側でAPIを叩くのは二度手間なので予めHTMLに埋め込んでおく
-misskeyApi('notes/show', {
-	noteId: props.noteId,
-}).then(res => {
-	// リモートのノートは埋め込ませない
-	if (res.url == null && res.uri == null) {
-		note.value = res;
-	}
-	loading.value = false;
-}).catch(err => {
-	console.error(err);
-	loading.value = false;
-});
+if (assertServerContext(serverContext, 'note')) {
+	note.value = serverContext.note;
+} else {
+	note.value = await misskeyApi('notes/show', {
+		noteId: props.noteId,
+	}).catch(() => {
+		return null;
+	});
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
index d9759a47e7..b481b3ebe5 100644
--- a/packages/frontend-embed/src/pages/tag.vue
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -38,14 +38,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { computed, shallowRef, inject } from 'vue';
+import { computed, inject, useTemplateRef } from 'vue';
 import { scrollToTop } from '@@/js/scroll.js';
 import type { Paging } from '@/components/EmPagination.vue';
 import EmNotes from '@/components/EmNotes.vue';
 import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
 import { i18n } from '@/i18n.js';
-import { serverMetadata } from '@/server-metadata.js';
 import { url, instanceName } from '@@/js/config.js';
 import { isLink } from '@@/js/is-link.js';
 import { DI } from '@/di.js';
@@ -55,6 +54,8 @@ const props = defineProps<{
 	tag: string;
 }>();
 
+const serverMetadata = inject(DI.serverMetadata)!;
+
 const embedParams = inject(DI.embedParams, defaultEmbedParams);
 
 const pagination = computed(() => ({
@@ -64,7 +65,7 @@ const pagination = computed(() => ({
 	},
 } as Paging));
 
-const notesEl = shallowRef<InstanceType<typeof EmNotes> | null>(null);
+const notesEl = useTemplateRef('notesEl');
 
 function top(ev: MouseEvent) {
 	const target = ev.target as HTMLElement | null;
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
index 8f587d2604..2d5dbb687b 100644
--- a/packages/frontend-embed/src/pages/user-timeline.vue
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -5,8 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<EmLoading v-if="loading"/>
-	<EmTimelineContainer v-else-if="user" :showHeader="embedParams.header">
+	<EmTimelineContainer v-if="user && !prohibited" :showHeader="embedParams.header">
 		<template #header>
 			<div :class="$style.userHeader">
 				<a :href="`/@${user.username}`" target="_blank" rel="noopener noreferrer" :class="$style.avatarLink">
@@ -46,21 +45,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script setup lang="ts">
-import { ref, computed, shallowRef, inject } from 'vue';
+import { ref, computed, inject, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url, instanceName } from '@@/js/config.js';
+import { defaultEmbedParams } from '@@/js/embed-page.js';
 import type { Paging } from '@/components/EmPagination.vue';
 import EmNotes from '@/components/EmNotes.vue';
 import EmAvatar from '@/components/EmAvatar.vue';
-import EmLoading from '@/components/EmLoading.vue';
 import EmUserName from '@/components/EmUserName.vue';
 import I18n from '@/components/I18n.vue';
 import XNotFound from '@/pages/not-found.vue';
 import EmTimelineContainer from '@/components/EmTimelineContainer.vue';
 import { misskeyApi } from '@/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { serverMetadata } from '@/server-metadata.js';
-import { url, instanceName } from '@@/js/config.js';
-import { defaultEmbedParams } from '@@/js/embed-page.js';
+import { assertServerContext } from '@/server-context.js';
 import { DI } from '@/di.js';
 
 const props = defineProps<{
@@ -69,26 +67,37 @@ const props = defineProps<{
 
 const embedParams = inject(DI.embedParams, defaultEmbedParams);
 
-const user = ref<Misskey.entities.UserLite | null>(null);
+const serverMetadata = inject(DI.serverMetadata)!;
+
+const serverContext = inject(DI.serverContext)!;
+
+const user = ref<Misskey.entities.UserLite | null>();
+
+const prohibited = ref(false);
+
+if (assertServerContext(serverContext, 'user')) {
+	user.value = serverContext.user;
+} else {
+	user.value = await misskeyApi('users/show', {
+		userId: props.userId,
+	}).catch(() => {
+		return null;
+	});
+}
+
+if (user.value?.host != null) {
+	// リモートサーバーのユーザーは弾く
+	prohibited.value = true;
+}
+
 const pagination = computed(() => ({
 	endpoint: 'users/notes',
 	params: {
 		userId: user.value?.id,
 	},
 } as Paging));
-const loading = ref(true);
-
-const notesEl = shallowRef<InstanceType<typeof EmNotes> | null>(null);
-
-misskeyApi('users/show', {
-	userId: props.userId,
-}).then(res => {
-	user.value = res;
-	loading.value = false;
-}).catch(err => {
-	console.error(err);
-	loading.value = false;
-});
+
+const notesEl = useTemplateRef('notesEl');
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts
new file mode 100644
index 0000000000..a84a1a726a
--- /dev/null
+++ b/packages/frontend-embed/src/server-context.ts
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import * as Misskey from 'misskey-js';
+
+const providedContextEl = document.getElementById('misskey_embedCtx');
+
+export type ServerContext = {
+	clip?: Misskey.entities.Clip;
+	note?: Misskey.entities.Note;
+	user?: Misskey.entities.UserLite;
+} | null;
+
+// NOTE: devモードのときしか embedCtx が null になることは無い
+export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
+
+export function assertServerContext<K extends keyof NonNullable<ServerContext>>(ctx: ServerContext, entity: K): ctx is Required<Pick<NonNullable<ServerContext>, K>> {
+	if (ctx == null) return false;
+	return entity in ctx;
+}
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
index 35d9946b12..f426778898 100644
--- a/packages/frontend-embed/src/ui.vue
+++ b/packages/frontend-embed/src/ui.vue
@@ -18,11 +18,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div
 		:class="$style.routerViewContainer"
 	>
-		<EmNotePage v-if="page === 'notes'" :noteId="contentId"/>
-		<EmUserTimelinePage v-else-if="page === 'user-timeline'" :userId="contentId"/>
-		<EmClipPage v-else-if="page === 'clips'" :clipId="contentId"/>
-		<EmTagPage v-else-if="page === 'tags'" :tag="contentId"/>
-		<XNotFound v-else/>
+		<Suspense :timeout="0">
+			<EmNotePage v-if="page === 'notes'" :noteId="contentId"/>
+			<EmUserTimelinePage v-else-if="page === 'user-timeline'" :userId="contentId"/>
+			<EmClipPage v-else-if="page === 'clips'" :clipId="contentId"/>
+			<EmTagPage v-else-if="page === 'tags'" :tag="contentId"/>
+			<XNotFound v-else/>
+			<template #fallback>
+				<EmLoading/>
+			</template>
+		</Suspense>
 	</div>
 </div>
 </template>
@@ -37,6 +42,7 @@ import EmUserTimelinePage from '@/pages/user-timeline.vue';
 import EmClipPage from '@/pages/clip.vue';
 import EmTagPage from '@/pages/tag.vue';
 import XNotFound from '@/pages/not-found.vue';
+import EmLoading from '@/components/EmLoading.vue';
 
 const page = location.pathname.split('/')[2];
 const contentId = location.pathname.split('/')[3];
-- 
cgit v1.3.1-freya

