summaryrefslogtreecommitdiff
path: root/packages/backend/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server')
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts46
-rw-r--r--packages/backend/src/server/web/boot.embed.js219
-rw-r--r--packages/backend/src/server/web/boot.js11
-rw-r--r--packages/backend/src/server/web/style.css1
-rw-r--r--packages/backend/src/server/web/style.embed.css99
-rw-r--r--packages/backend/src/server/web/views/base-embed.pug67
-rw-r--r--packages/backend/src/server/web/views/base.pug12
7 files changed, 433 insertions, 22 deletions
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index f55790b636..5e0ec390f2 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -61,7 +61,8 @@ const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../frontend/assets/`;
const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
-const viteOut = `${_dirname}/../../../../../built/_vite_/`;
+const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
+const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`;
@Injectable()
@@ -277,15 +278,22 @@ export class ClientServerService {
});
//#region vite assets
- if (this.config.clientManifestExists) {
+ if (this.config.frontendEmbedManifestExists) {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
- root: viteOut,
+ root: frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
+ fastify.register(fastifyStatic, {
+ root: frontendEmbedViteOut,
+ prefix: '/embed_vite/',
+ maxAge: ms('30 days'),
+ immutable: true,
+ decorateReply: false,
+ });
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
});
@@ -296,6 +304,13 @@ export class ClientServerService {
prefix: '/vite',
rewritePrefix: '/vite',
});
+
+ const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
+ fastify.register(fastifyProxy, {
+ upstream: 'http://localhost:' + embedPort,
+ prefix: '/embed_vite',
+ rewritePrefix: '/embed_vite',
+ });
}
//#endregion
@@ -425,6 +440,13 @@ export class ClientServerService {
// Manifest
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
+ // Embed Javascript
+ fastify.get('/embed.js', async (request, reply) => {
+ return await reply.sendFile('/embed.js', staticAssets, {
+ maxAge: ms('1 day'),
+ });
+ });
+
fastify.get('/robots.txt', async (request, reply) => {
return await reply.sendFile('/robots.txt', staticAssets);
});
@@ -762,7 +784,7 @@ export class ClientServerService {
});
//#endregion
- //region noindex pages
+ //#region noindex pages
// Tags
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
@@ -772,7 +794,20 @@ export class ClientServerService {
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
- //endregion
+ //#endregion
+
+ //#region embed pages
+ fastify.get('/embed/*', async (request, reply) => {
+ const meta = await this.metaService.fetch();
+
+ reply.removeHeader('X-Frame-Options');
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: meta.name ?? 'Misskey',
+ ...await this.generateCommonPugData(meta),
+ });
+ });
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
@@ -787,6 +822,7 @@ export class ClientServerService {
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
});
});
+ //#endregion
fastify.get('/bios', async (request, reply) => {
return await reply.view('bios', {
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
new file mode 100644
index 0000000000..48d1cd262b
--- /dev/null
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -0,0 +1,219 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+'use strict';
+
+// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
+(async () => {
+ window.onerror = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED');
+ };
+ window.onunhandledrejection = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED_IN_PROMISE');
+ };
+
+ let forceError = localStorage.getItem('forceError');
+ if (forceError != null) {
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+ return;
+ }
+
+ // パラメータに応じてsplashのスタイルを変更
+ const params = new URLSearchParams(location.search);
+ if (params.has('rounded') && params.get('rounded') === 'false') {
+ document.documentElement.classList.add('norounded');
+ }
+ if (params.has('border') && params.get('border') === 'false') {
+ document.documentElement.classList.add('noborder');
+ }
+
+ //#region Detect language & fetch translations
+ if (!localStorage.hasOwnProperty('locale')) {
+ const supportedLangs = LANGS;
+ let lang = localStorage.getItem('lang');
+ if (lang == null || !supportedLangs.includes(lang)) {
+ if (supportedLangs.includes(navigator.language)) {
+ lang = navigator.language;
+ } else {
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+ // Fallback
+ if (lang == null) lang = 'en-US';
+ }
+ }
+
+ const metaRes = await window.fetch('/api/meta', {
+ method: 'POST',
+ body: JSON.stringify({}),
+ credentials: 'omit',
+ cache: 'no-cache',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ if (metaRes.status !== 200) {
+ renderError('META_FETCH');
+ return;
+ }
+ const meta = await metaRes.json();
+ const v = meta.version;
+ if (v == null) {
+ renderError('META_FETCH_V');
+ return;
+ }
+
+ // for https://github.com/misskey-dev/misskey/issues/10202
+ if (lang == null || lang.toString == null || lang.toString() === 'null') {
+ console.error('invalid lang value detected!!!', typeof lang, lang);
+ lang = 'en-US';
+ }
+
+ const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
+ if (localRes.status === 200) {
+ localStorage.setItem('lang', lang);
+ localStorage.setItem('locale', await localRes.text());
+ localStorage.setItem('localeVersion', v);
+ } else {
+ renderError('LOCALE_FETCH');
+ return;
+ }
+ }
+ //#endregion
+
+ //#region Script
+ async function importAppScript() {
+ await import(`/embed_vite/${CLIENT_ENTRY}`)
+ .catch(async e => {
+ console.error(e);
+ renderError('APP_IMPORT');
+ });
+ }
+
+ // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
+ if (document.readyState !== 'loading') {
+ importAppScript();
+ } else {
+ window.addEventListener('DOMContentLoaded', () => {
+ importAppScript();
+ });
+ }
+ //#endregion
+
+ async function addStyle(styleText) {
+ let css = document.createElement('style');
+ css.appendChild(document.createTextNode(styleText));
+ document.head.appendChild(css);
+ }
+
+ async function renderError(code) {
+ // Cannot set property 'innerHTML' of null を回避
+ if (document.readyState === 'loading') {
+ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+ }
+ document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
+ <div class="message">読み込みに失敗しました</div>
+ <div class="submessage">Failed to initialize Misskey</div>
+ <div class="submessage">Error Code: ${code}</div>
+ <button onclick="location.reload(!0)">
+ <div>リロード</div>
+ <div><small>Reload</small></div>
+ </button>`;
+ addStyle(`
+ #misskey_app,
+ #splash {
+ display: none !important;
+ }
+
+ html,
+ body {
+ margin: 0;
+ }
+
+ body {
+ position: relative;
+ color: #dee7e4;
+ font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ line-height: 1.35;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 24px;
+ box-sizing: border-box;
+ overflow: hidden;
+
+ border-radius: var(--radius, 12px);
+ border: 1px solid rgba(231, 255, 251, 0.14);
+ }
+
+ body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #192320;
+ border-radius: var(--radius, 12px);
+ z-index: -1;
+ }
+
+ html.embed.norounded body,
+ html.embed.norounded body::before {
+ border-radius: 0;
+ }
+
+ html.embed.noborder body {
+ border: none;
+ }
+
+ .icon {
+ max-width: 60px;
+ width: 100%;
+ height: auto;
+ margin-bottom: 20px;
+ color: #dec340;
+ }
+
+ .message {
+ text-align: center;
+ font-size: 20px;
+ font-weight: 700;
+ margin-bottom: 20px;
+ }
+
+ .submessage {
+ text-align: center;
+ font-size: 90%;
+ margin-bottom: 7.5px;
+ }
+
+ .submessage:last-of-type {
+ margin-bottom: 20px;
+ }
+
+ button {
+ padding: 7px 14px;
+ min-width: 100px;
+ font-weight: 700;
+ font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ line-height: 1.35;
+ border-radius: 99rem;
+ background-color: #b4e900;
+ color: #192320;
+ border: none;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ button:hover {
+ background-color: #c6ff03;
+ }`);
+ }
+})();
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 5283596316..7c6a533429 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-/**
- * BOOT LOADER
- * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
- * - 翻訳ファイルをフェッチする。
- * - バージョンに基づいて適切なメインスクリプトを読み込む。
- * - キャッシュされたコンパイル済みテーマを適用する。
- * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
- * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
- * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
- */
-
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css
index e4723c24fd..dbcc8f537c 100644
--- a/packages/backend/src/server/web/style.css
+++ b/packages/backend/src/server/web/style.css
@@ -47,6 +47,7 @@ html {
transform: translateY(70px);
color: var(--accent);
}
+
#splashSpinner > .spinner {
position: absolute;
top: 0;
diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css
new file mode 100644
index 0000000000..a7b110d80a
--- /dev/null
+++ b/packages/backend/src/server/web/style.embed.css
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+html {
+ background-color: var(--bg);
+ color: var(--fg);
+}
+
+html.embed {
+ box-sizing: border-box;
+ background-color: transparent;
+ color-scheme: light dark;
+ max-width: 500px;
+}
+
+#splash {
+ position: fixed;
+ z-index: 10000;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ cursor: wait;
+ background-color: var(--bg);
+ opacity: 1;
+ transition: opacity 0.5s ease;
+}
+
+html.embed #splash {
+ box-sizing: border-box;
+ min-height: 300px;
+ border-radius: var(--radius, 12px);
+ border: 1px solid var(--divider, #e8e8e8);
+}
+
+html.embed.norounded #splash {
+ border-radius: 0;
+}
+
+html.embed.noborder #splash {
+ border: none;
+}
+
+#splashIcon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ width: 64px;
+ height: 64px;
+ pointer-events: none;
+}
+
+#splashSpinner {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ transform: translateY(70px);
+ color: var(--accent);
+}
+
+#splashSpinner > .spinner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 28px;
+ height: 28px;
+ fill-rule: evenodd;
+ clip-rule: evenodd;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-miterlimit: 1.5;
+}
+#splashSpinner > .spinner.bg {
+ opacity: 0.275;
+}
+#splashSpinner > .spinner.fg {
+ animation: splashSpinner 0.5s linear infinite;
+}
+
+@keyframes splashSpinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug
new file mode 100644
index 0000000000..d773f2676a
--- /dev/null
+++ b/packages/backend/src/server/web/views/base-embed.pug
@@ -0,0 +1,67 @@
+block vars
+
+block loadClientEntry
+ - const entry = config.frontendEmbedEntry;
+
+doctype html
+
+html(class='embed')
+
+ head
+ meta(charset='utf-8')
+ meta(name='application-name' content='Misskey')
+ meta(name='referrer' content='origin')
+ meta(name='theme-color' content= themeColor || '#86b300')
+ meta(name='theme-color-orig' content= themeColor || '#86b300')
+ meta(name='viewport' content='width=device-width, initial-scale=1')
+ meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
+ link(rel='icon' href= icon || '/favicon.ico')
+ link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
+ link(rel='modulepreload' href=`/embed_vite/${entry.file}`)
+
+ if !config.frontendEmbedManifestExists
+ script(type="module" src="/embed_vite/@vite/client")
+
+ if Array.isArray(entry.css)
+ each href in entry.css
+ link(rel='stylesheet' href=`/embed_vite/${href}`)
+
+ title
+ block title
+ = title || 'Misskey'
+
+ block meta
+ meta(name='robots' content='noindex')
+
+ style
+ include ../style.embed.css
+
+ script.
+ var VERSION = "#{version}";
+ var CLIENT_ENTRY = "#{entry.file}";
+
+ script(type='application/json' id='misskey_meta' data-generated-at=now)
+ != metaJson
+
+ script
+ include ../boot.embed.js
+
+ body
+ noscript: p
+ | JavaScriptを有効にしてください
+ br
+ | Please turn on your JavaScript
+ div#splash
+ img#splashIcon(src= icon || '/static-assets/splash.png')
+ div#splashSpinner
+ <svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
+ <g transform="matrix(1,0,0,1,12,12)">
+ <circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
+ </g>
+ </svg>
+ <svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
+ <g transform="matrix(1,0,0,1,12,12)">
+ <path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
+ </g>
+ </svg>
+ block content
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index da6d1eafd3..88714b2556 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -1,7 +1,7 @@
block vars
block loadClientEntry
- - const clientEntry = config.clientEntry;
+ - const entry = config.frontendEntry;
doctype html
@@ -36,13 +36,13 @@ html
link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
- link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
+ link(rel='modulepreload' href=`/vite/${entry.file}`)
- if !config.clientManifestExists
+ if !config.frontendManifestExists
script(type="module" src="/vite/@vite/client")
- if Array.isArray(clientEntry.css)
- each href in clientEntry.css
+ if Array.isArray(entry.css)
+ each href in entry.css
link(rel='stylesheet' href=`/vite/${href}`)
title
@@ -68,7 +68,7 @@ html
script.
var VERSION = "#{version}";
- var CLIENT_ENTRY = "#{clientEntry.file}";
+ var CLIENT_ENTRY = "#{entry.file}";
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson