Contributing web UI with Plugins
プラグインは、Owncastに自分のユーザーインターフェイスを2つの場所に追加できます:管理者内のタブ(ストリーマー向けの設定)と、ストリーム下のアクションボタン(視聴者向けのアクション)。 両方の宣言はマニフェストに記載され、ホストによって管理されます。 あなたがコンテンツを送り、Owncastがそれを正しいクルームに配置します。
このページのマニフェスト宣言はプレーンなJSONで、記述する言語に関係なく同じです。 動的コンテンツハンドラとランタイムコールは、両方のSDKで表示されます。 JavaScriptまたはPythonを参照して、インストールとセットアップを行ってください。
管理ページ
フラットで型付きの設定(文字列、数字、スイッチ)の場合、マニフェストのconfigブロックを宣言し、Owncastにフォームをレンダリングさせます。 設定を参照してください。 自動フォームで表現できないUIが必要な場合、カスタム管理ページを作成します。
プラグインは、Owncast管理UIのプラグイン内に表示されるページを登録できます。 Declare them as an object keyed by plugin-relative path glob:
{
"permissions": ["http.serve"],
"admin": {
"pages": {
"/admin": { "title": "Settings", "icon": "gear" }
}
}
}
Each entry has:
| Part | ノート |
|---|---|
| object key | Required path glob under /plugins/\<your-slug>/. 例:"/admin"、"/admin/*"、"/admin/api/*"。 |
title | Required tab label inside the plugin's admin view. |
icon | Optional short semantic name. サポートされている:gear、wrench、user、users、lock、info、apps、docs、bell(settingsやnotificationsのようなエイリアスも機能します)。 |
The host derives the page path from the object key. Do not add a path member to the value. The host rejects arrays and page values containing the legacy path member.
どのようにレンダリングされるか
Owncastの管理は、宣言された各ページを/admin/plugins/configure?id=\<your-slug>内のタブとしてレンダリングします。 The tab body is an \<iframe> pointed at the path from the object key under /plugins/\<your-slug>/. 各プラグインには、ブックマーク可能なURLと管理ナビゲーションのプラグインの下のサイドバーエントリーが与えられます。
ホストは、管理パスのHTMLレスポンスにベースラインスタイルシートを自動的に注入するため、プレーンな<input>や\<button>コントロールは、CSSを提供することなくOwncastの管理にネイティブな見た目を持ちます。 プラグインUIのスタイリングを参照して、無料で得られるものや使用可能なヘルパークラスをチェックしてください。 独自のスタイリングを好むプラグインは、上乗せすることができます。
サンドボックス
このページはサンドボックス化された\<iframe>内で実行されます。 あなたのスクリプトが実行され、フォームが送信され、同一オリジンのfetchがあなた自身の/plugins/\<your-slug>/エンドポイントに対して行われます。 Pages may also open popups, trigger file downloads (e.g. a blob or data-URL \<a download> you click from script), and use confirm() / alert() / prompt() dialogs. サンドボックスは、通常気づく唯一の制約です。 ブラウザ機能が静かにブロックされていると思われる場合、最初に確認すべきことはiframeのサンドボックスです。
認証ゲート
マニフェストに宣言された管理パスへのリクエストはホストによって認証ゲートされます。 認証されていないリクエストは、プラグインコードが実行される前に401を受け取ります。 これらのパスについてはリクエストの認証を確認する必要はありません。
静的ファイルとマッチしたパスの動的エンドポイントは、どちらも認証ゲートされています。 同じゲートはあなたのpublic/admin/index.htmlおよびPOST /admin/api/save-settingsにも適用されます。
UIページとJSON APIの両方がある場合は、複数のグロブを使用します:
{
"admin": {
"pages": {
"/admin": { "title": "Settings" },
"/admin/*": { "title": "Settings" }
}
}
}
The admin UI deduplicates tabs by the resolved iframe URL, not by title. /admin and /admin/* both resolve to /admin/, so this pair produces one visible tab that gates the whole subtree. A pair like /admin and /admin/api/* resolves to two different URLs and produces two tabs. JSON object order is not significant. Owncast processes and displays pages in lexicographic path order.
著者フロー
- 管理者HTML、CSS、JSは
public/admin/index.html(およびその関連)に配置します。 - リクエストハンドラを介して管理者APIを
/admin/api/...で公開します(HTTPの提供を参照)。 - Declare the relevant path keys in
manifest.admin.pages. - 管理UIで
/admin/plugins/configure?id=\<your-slug>を訪れます。 Owncastはあなたの既存の管理ログインを使用してページをゲートします。 追加のプロンプトはありません。
アクションボタン
Owncastは、視聴者UIにアクションボタンの行を表示します。 URLを開いたり(モーダルまたは新しいタブで)生のHTMLをレンダリングしたりするクリック可能なエントリーです。 プラグインは独自のものを追加できます。
マニフェストで宣言されたボタン
{
"permissions": ["ui.modify", "http.serve"],
"actions": [
{
"title": "Chat Overlay",
"description": "Open the live chat overlay",
"url": "/",
"icon": "/star.png",
"color": "#3b82f6"
},
{
"title": "Issue tracker",
"url": "https://github.com/example/my-plugin/issues",
"openExternally": true
},
{
"title": "About this stream",
"html": "<p>Live every weekday at 8pm UTC.</p>"
}
]
}
プラグインが有効な間は、ホストがそのアクションエントリーをOwncastがすでにストリームの下に表示しているリストに統合します。 無効にすると、それらは消えます。
フィールド参照
| フィールド | ノート |
|---|---|
title | 必須。 ボタンラベル。 |
url | 絶対的なhttps://... URLまたはパスのいずれかです。 htmlとは相互に排他的です。 |
html | インラインモーダルでレンダリングされる生のHTML。 urlとは相互に排他的です。 |
icon | ボタンに表示されるオプションの画像URL。 urlと同じパスルール。 |
color | ボタン背景のオプションのヘックスカラー。 |
description | オプション。 URLベースのアクションのために開くモーダルで表示されます。 |
openExternally | trueの場合、URLはインラインモーダルではなく新しいタブで開きます。 |
パスルール
2つのシンプルなルールですべてをカバーします:
- 相対パスは自動的にプラグインの名前空間にプレフィックスが付けられます。
"/"は/plugins/my-plugin/になります。"/star.png"は/plugins/my-plugin/star.pngになります。 これにより、プラグイン名をハードコーディングする必要がなくなります。urlとiconの両方に適用されます。 - 絶対的な
https://...URLはそのまま通過します。 これを外部リンクやCDNホスティングされたアイコンに使用します。
ホストは次のことを強制します:
ui.modifyの権限が必要です。actionsを持っているがui.modifyを持っていないマニフェストは読み込み時に拒否されます。- 各エントリーには
urlまたはhtmlのいずれかが正確に1つ必要です。 - あなたの名前空間に解決されるURLとアイコンには
http.serveが必要です。 それらを提供するのはあなたです。 - 他のプラグインの名前空間を指すURLやアイコンは拒否されます。 タイプミスを検出し、1つのプラグインが他のプラグインのユーザーインターフェイスを宣伝するのを防ぎます。
ランタイムの追加
プラグインは、再読み込みなしにowncast.actions.add(...)を呼ぶことで、ランタイムでさらにアクションボタンを追加できます。 ランタイムエントリーはすべてmanifest.actionsと同じバリデーションを経て、プラグインの設定に保持されるため、追加は再読み込みで持続します。 owncast.actions.clear()はすべてのランタイム追加を削除します。 マニフェストで宣言されたアクションは残ります。
- JavaScript
- Python
const { definePlugin, owncast } = require('@owncast/plugin-sdk');
module.exports = definePlugin({
onStreamStarted() {
owncast.actions.add({
title: 'Donate',
url: 'https://example.com/donate',
openExternally: true,
});
// or add several at once: owncast.actions.add([ { ... }, { ... } ])
},
});
from owncast_plugin import plugin, owncast
@plugin.on_stream_started
def add_button(info):
owncast.actions.add({
"title": "Donate",
"url": "https://example.com/donate",
"openExternally": True,
})
# or add several at once: owncast.actions.add([ { ... }, { ... } ])
一般的なパターンは、ストリーマーがプラグインのデフォルトに追加するカスタムボタン(ラベル+ URL)を追加できる管理ページです。 SDKのaction-buttonsの例には、この作業バージョンが含まれています。
プラグインUIのスタイリング
Owncastは、iframe内でレンダリングされるすべてのプラグインサーフェスに基本スタイルシートを注入します:あなたの管理ページとあなたの視聴者ページタブ。 これはOwncastの独自のデザイントークンに基づいているため、プレーンなセマンティックHTMLは、自分自身のCSSなしでネイティブな外観を採用します。
- 見出し、段落、およびリンクはテーマフォントと色を引き受けます。
<input>、\<textarea>、\<select>、および\<button>は、ネイティブコントロールのようにレンダリングされます。\<button>は主要なスタイルを持ちます。 アウトラインバリアント用にclass="secondary"を追加します。\<table>、\<fieldset>、および\<code>/\<pre>は意味のあるネイティブスタイリングを取得します。
あなたのコンテンツはページにフラッシュしています。 iframeの背景は透明なので、ホストのパネルが表示され、組み込みの「アバウト」と「フォロワー」タブの表示方式と同じです。 あなたは、そして追加するべきではありません、な opaque ページ背景やすべての周りにラッピングボックス。 そのフラッシュレンダリングは、プラグインタブが埋め込まれたフレームではなく、Owncastの一部として読むことを可能にします。
ヘルパークラス
プレーンな要素を超えるネイティブビルディングブロックには、基本にいくつかのオプトインクラスがあります。 それらは、他のOwncastと同じテーマトークンを参照しているため、管理者がテーマをカスタマイズすると自動的に再スタイルされます。
| クラス | それが行うこと |
|---|---|
card | ネイティブカードのサーフェス、フォロワーやフィーチャーストリームカードと同じ外観。 プレーンな\<section> / \<article>がフラッシュのままで、ボックスの表面が必要な場合はclass="card"を選択してください。 |
card interactive | ネイティブホバーレフトのために、クリック可能なカードにinteractiveを追加します。 |
card-grid | レスポンシブグリッドは、できるだけ多くの約260pxの列を埋め、狭いフレームでは1列に折りたたまれます。 cardの子要素を直接追加します。 |
tag | ネイティブストリームカードのタグに合ったピルタグまたはバッジ。 |
stack | 一貫したギャップのある垂直フレックス列。 |
row | 一貫したギャップのある水平フレックス行。 |
muted | キャプションや二次的な詳細のための強調を減らしたテキスト。 |
<div class="card-grid">
<article class="card interactive">
<h3>Album A</h3>
<p class="muted">Artist A</p>
<div class="row">
<span class="tag">jazz</span>
<span class="tag">2024</span>
</div>
</article>
<article class="card interactive">
<h3>Album B</h3>
<p class="muted">Artist B</p>
</article>
</div>
ここにあるすべてはオプトインです。 何も持たずに送られてくるタブは、すでにネイティブに見えます。 Owncastの値を手動でコピーすることなく、カード、グリッド、またはタグを必要なときにヘルパーを利用し、自分のCSSを重ね合わせてください(必要なものがベースラインでカバーされていないときはビューアスタイルシートを参照)。
ビューアスタイルシート
プラグインは、CSSファイルをバンドルし、それをmanifest.stylesにリストすることで、ビューアページのテーマを設定できます。 ホストは、各ファイルの内容をページ上の単一のプラグインスタイルブロックにインライン化するため、プラグインは、自身の<link>タグを必要とすることなく、ページのCSSを拡張します。
{
"permissions": ["ui.modify"],
"styles": ["theme.css", "overrides.css"]
}
ui.modifyのみが必要です(プラグインはOwncastのクローム内にペイントします)。 http.serveは必要ありません:ホストは、プラグインのassets/ディレクトリから各ファイルを読み取り、バイナリを/api/configのページのプラグインスタイルブロックにインラインします。
パスルール
"theme.css"のような生のパスは、プラグインの名前空間に自動的に接頭辞を付けます。"/theme.css"も同様に解決されます。- 完全修飾された
/plugins/\<your-slug>/...パスは通過します。 - 他のプラグインの名前空間にあるパスは拒否されます。
http://およびhttps://のURLは拒否されます。 外部アセットをバンドルし、CSSの中で@font-faceまたはurl(...)で参照してください。そうすれば、マニフェストを確認している管理者は読み込まれるすべてのファイルを確認できます。- 各エントリは
.cssで終わる必要があります。
貢献がどのようにレンダリングされるか
The host reads each file at request time and concatenates the bytes in front of an /* plugin: \<your-slug> ... */ comment, so devtools "view source" attributes a rule back to the plugin that shipped it. プラグインを無効にすると、次回ページを読み込むとき、その貢献が落ちます。
CSSボディはライブビューワーDOMに対して実行されるため、セレクタがページのレンダリングした内容にターゲットを設定します。 単一のルートIDの下で全てのルールを範囲指定することは、保持すべき防御的な習慣です。 これがなければ、ルールがホストページがレンダリングする要素に一致し、有意義な回帰を生む可能性があります。
プラグインスタイルがカスケード内に存在する場所
ビューワーページは、次の4つのレイヤーから外観を構築します。 後の層が勝ちます。
- Owncastのビルトインデフォルト。
- プラグインスタイル:最初に
manifest.stylesファイル、その後onPageStylesの出力。 - 管理者の外観変数、一般設定→外観の下で設定された色。
- 管理者のカスタムCSS、その同じページのエディタ。
あなたのスタイルはレイヤー2で、したがって、管理者のレイヤー3および4の明示的な選択は、両方のプロパティに対してあなたのスタイルをオーバーライドします。 テーマは、最終的な発言よりもベースラインと見なすべきです:
- あなたが設定したトークンは、管理者がデフォルトのままにした場合、あなたの値を表示します。
- あなたが設定したトークンは、管理者も設定した場合、管理者の値を表示します。
部分的および完全なテーマはどちらも問題ありません。 リンクの色をのみ再彩色するプラグインは、他の色には影響しません。 全てのパレットを設定するプラグインでも、管理者が選択した各個別の色には従います。 管理者は自分のインスタンスを制御下に持ち、外観ページはプラグインが関与していることを知らせます:あなたのプラグインを名付けた通知を表示し、設定した各色にはalso set by \<plugin>の注記が付いています。 For that flagging to work, declare your colors as --theme-color-* custom properties in a :root { ... } block, the same form the admin's pickers write.
1つのエスケープハッチが順序を崩します:!importantでマークされたプラグインルールは、レイヤに関わらず管理者の通常の宣言を上回ります。 管理者が自分の色を最終的に支配できるようにしたい場合、テーマCSSでは避けてください。
注意:CSS内の相対URL
プラグインのCSS内のurl(...)の参照は、プラグインの名前空間ではなく、ビューアページに対して解決されます。 バンドルされた画像を参照する場合は、./logo.pngの代わりに絶対パス/plugins/\<your-slug>/logo.pngを使用してください。 @font-faceのソースにも同様です。 プラグインの静的URLスペースはサービスされるため、直接参照は機能しますが、<link>がファイルを指していなくても動作します。
動的スタイルシート:onPageStyles
CSSがプラグインの状態、管理者が選択したテーマ、またはKVストアの値に依存する場合、静的ファイルの代わりにonPageStylesハンドラからそれを返してください。 これに対するマニフェストフィールドは存在しません。 ホストは、ui.modifyを保持し、エクスポートするプラグインごとに/api/configを1回呼び出し、その結果を静的なmanifest.stylesファイルの後にあなたのプラグインスタイルブロックに追加します。 プラグイン自身のスタイル内では、後のルールが勝ちます。したがって、onPageStylesからアクティブなオーバーライドだけを返すことが十分です。 全ブロックは、管理者の外観設定の下に位置します(プラグインスタイルがカスケード内に存在する場所を参照)。
- JavaScript
- Python
const ACCENTS = { ocean: '#2386e2', forest: '#42bea6' };
module.exports = definePlugin({
onPageStyles() {
const accent = ACCENTS[owncast.kv.get('theme')];
if (!accent) return;
return `:root { --theme-color-action: ${accent}; }`;
},
});
ACCENTS = {"ocean": "#2386e2", "forest": "#42bea6"}
@plugin.on_page_styles
def page_styles():
accent = ACCENTS.get(owncast.kv.get("theme"))
if not accent:
return
return f":root {{ --theme-color-action: {accent}; }}"
ui.modifyが必要です。 The examples above also read the KV store, which separately requires storage.kv. 指定されたリクエストで貢献が何もない場合は、何も返さなくてください(空のreturn、つまり""を返すのと同じです)。 呼び出しには、ビューアごとの引数は含まれないため、/api/configの応答はキャッシュ可能です。 SDK内のtheme-hubの例では、管理者が選択したテーマをビューアUI全体に適用するためにこれを使用します。
ビューアスクリプト
プラグインはJavaScriptファイルをバンドルし、それをmanifest.scriptsにリストすることで、ビューアページの実行時を拡張できます。 各ファイルの内容は、すでに管理者のカスタムJS用にOwncastが提供している/customjavascript応答に追加されるため、プラグインは、各貢献が自身の\<script>タグを必要とすることなく、ページの動作を拡張できます。
{
"permissions": ["ui.modify"],
"scripts": ["client.js"]
}
stylesと同様の許可とパスルールが、単一の.jsエントリに適用されます(ui.modifyのみが必要で、ホストはassets/から読み取って/customjavascriptにインラインします)。 各貢献には// plugin: \<your-slug> ...というコメントが付けられます。
これらは、ブラウザで実行されるビューアページスクリプトであり、サーバーサイドプラグインで記述された言語に関係なく、常にJavaScriptです。
実行コンテキスト
ビューアページは、単一の\<script async>タグとして/customjavascriptを読み込みます。 すべてのプラグインのJSは、管理者のカスタムJSやOwncastのクロームの残りの部分と同じグローバルウィンドウ内で実行されます。 三つの含意:
- トップレベルの
varおよびfunction宣言はwindowに留まります。 Wrap your script in an IIFE ((function(){ ... })()) so private state stays private and you don't collide with the admin's JS or other plugins. - ホストは各プラグインの貢献を自身のtry/catchでラップするため、ランタイムエラーは他のプラグインのスクリプトを停止せずにブラウザコンソールにスローされます(プレフィックス
owncast plugin \<your-slug> script error:)。 構文エラーは隔離されず、1つの連結スクリプトタグの解析を中断させますので、有効なJavaScriptを提供してください。 - 相対
fetch('./data.json')は、ビューアページのURLに対して解決され、あなたのプラグインに対しては解決されません。public/で配布するファイルには、/plugins/\<your-slug>/data.jsonのような絶対パスを使用してください。
動的スクリプト:onPageScripts
onPageStylesのスクリプト対応部分。 リクエスト時に計算されたJavaScriptを、マニフェストフィールドなしでonPageScriptsハンドラから返します。 ホストは、ui.modifyを保持しエクスポートする任意のプラグインごとに/api/configを1回呼び出し、その結果を静的なmanifest.scriptsファイルの後に/customjavascriptに追加します。同じプラグインごとにtry/catchで囲まれます。
これは、必ずしもテーマのみのリクエスト時のJavaScriptに関するものではありません。 ビューワー側のコードをリクエストごとに実行するために使用します。例えば、プラグインのKVストアで管理者が設定した値を表示します。 以下の例は、その値を視聴者に示します:
- JavaScript
- Python
module.exports = definePlugin({
// Run request-time JavaScript on the viewer page.
onPageScripts() {
const notice = owncast.kv.get('notice');
if (!notice) return;
return `alert(${JSON.stringify(notice)});`;
},
});
import json
@plugin.on_page_scripts
def page_scripts():
notice = owncast.kv.get("notice")
if not notice:
return
return f"alert({json.dumps(notice)});"
出力は共有されたビューアwindowで実行されるため、上記のIIFEおよび絶対パスアドバイスが依然として適用されます。 Escape any untrusted strings you embed: JSON.stringify in JavaScript and json.dumps in Python both produce a safely-quoted literal, which is why the examples wrap the notice in one before passing it to alert. Like the styles examples, reading the KV store requires storage.kv on top of ui.modify. 何も貢献しない場合は、何も返さないでください(空のreturn、つまり""を返すのと同じです)。
いつ使用するか
scriptsは、ビューアーサイドの状態に反応する必要があるプラグイン、ページの上に自身のUIをマウントする、またはプラグインが/plugins/\<your-slug>/で実行するバックエンドと話す必要があるプラグインに適したツールです。 チャット駆動のボット、メッセージフィルタ、サーバーサイドで実行すべき論理には、通常のプラグインハンドラの方が適しています。 それらはホストのサンドボックス内で動作し、ビューアページが到達できないOwncast APIと通信し、ユーザー制御のDOMを信頼しません。
追加ページコンテンツ
プラグインは、ビューアページの追加コンテンツブロックにHTMLを追加できます。 manifest.extraPageContentを、必須のslugとオプションのcontentパスを持つオブジェクトとして宣言します:
{
"permissions": ["ui.modify"],
"extraPageContent": { "slug": "banner", "content": "content.html" }
}
| フィールド | ノート |
|---|---|
slug | 必須です。 ホストがレンダリングされたHTMLを要求したときにページコンテンツハンドラに渡される安定した識別子。 小文字、数字、ハイフンのみ、初めは文字から開始します。 |
content | オプションです。 assets/内の静的HTMLファイルへの相対パス。 存在する場合、そのファイルのバイナリは直接インラインされます。 省略された場合、ホストはあなたのページコンテンツハンドラを呼び出します。 |
静的と動的
HTMLがすべてのビューアに対して同じ場合はcontentを使用します:発表のストリップ、スポンサーのバナー、文のブロック。 ビューアごとに内容が変更される必要がある場合は、またはライブデータを引き出すときはcontentを省略し、ページコンテンツハンドラを実装してください。 ホストは要求されたslugおよびビューアのアイデンティティを使ってハンドラを呼び出し、あなたのハンドラがレンダリングするHTML文字列を返します:
- JavaScript
- Python
module.exports = definePlugin({
onPageContent(ctx) {
if (ctx.slug === 'banner') {
const who = ctx.user ? `, ${ctx.user.displayName}` : '';
return `<div class="banner">Welcome${who}!</div>`;
}
return '';
},
});
@plugin.on_page_content("banner")
def banner(ctx):
name = ctx.user.display_name if ctx.user else None
who = f", {name}" if name else ""
return f'<div class="banner">Welcome{who}!</div>'
Handlers: page contentでペイロードの形状を確認してください。 ビューアのアイデンティティは、ビューアが認証されている場合に存在し、匿名のビューアに対しては存在しません。
ui.modifyが必要です。 http.serveは必要ありません:HTMLは/api/configの応答にインラインされ、URLとして提供されることはありません。
バイトは、追加コンテンツブロックの上部に配置され、管理者が構成したプローズの上にあります。 Each contribution is wrapped with an <!-- plugin: <your-slug> ... --> comment for attribution. 複数のプラグインの貢献は、ホストがロードした順序でスタックされます。
パスルール
stylesおよびscriptsと同様に、単一の.htmlエントリに適用されます。 プラグインごとに1つのファイル。 複数の異なるブロックを持ちたい場合は、1つのファイルからそれらをリンクまたは\<iframe>で読み込んでください。
MarkdownとHTML
管理者の追加ページコンテンツは、レンダリング前にOwncastのMarkdownプロセッサを通過します。 プラグインHTMLは通過しません:ホストは最初に管理者のコンテンツに対してMarkdownプロセッサを実行し、その後、あなたの生のバイトに追加します。 タグ、属性、インラインスクリプトは書かれた通りに通過します。
これは、プラグインHTMLがビューアページが受け入れる任意の要素を使用できることを意味します。 不適切なタグは周囲のクロームを壊す可能性があるため、埋め込む信頼されていない文字列(ユーザー名、取得したテキスト、自分の管理外のもの)はすべてエスケープしてください。
scriptsとの組み合わせ
extraPageContentはscriptsとペアになったときに輝きます:レビュー可能な形でHTMLとしてマークアップを発送し、宣言した要素からJavaScriptで相互作用をワイヤリングします。 ホストは、スクリプトが実行される前にHTMLをロードするため、document.getElementById(...)でプラグインが貢献する要素をターゲットにするスクリプトがタイミングトリックなしで機能します。
{
"permissions": ["ui.modify", "http.serve"],
"extraPageContent": { "slug": "panel", "content": "panel.html" },
"scripts": ["panel.js"]
}
これは、scriptsのみによるプラグインから同じDOMを命令的に構築するよりも、しばしばクリーンに読み取れるパターンです:
panel.htmlは構造、クラス、およびIDをプレーンHTMLとして理解できるように宣言します。panel.css(stylesに宣言)それをテーマ付けします。panel.jsはイベントリスナーを取り付け、データを取得し、状態を変化させます。
HTMLとJavaScriptを使用すべき時: 非自明なレイアウト、ARIA属性、または既存のDOMからブートストラップすることを期待するサードパーティのウィジェットに関するすべてのこと。 純粋なscriptsは、UIを特定の条件(フェッチ後、ユーザーアクション後)でのみ構築するプラグインにとっては依然として意味があります。初期描画時に何も描画しないのが正しい動作です。
extraPageContentが単独で十分な場合
スタンドアロンのextraPageContentは、発表ストリップ、スポンサーのバナー、イベントに反応する必要のない任意のブロックにとって最も簡単な方法です: マークアップを直接送信し、スクリプトを必要とせず、JavaScriptが無効なビューワーでも生き残ります。
ビューワーページのタブ
Plugins can add tabs to the viewer page's tab row next to the built-in About and Followers tabs by declaring manifest.tabs as an object. Each object key is the tab's stable slug. Every value requires a title, and content is optional.
{
"permissions": ["ui.modify"],
"tabs": {
"music": { "title": "Music", "content": "music.html" },
"stream-info": { "title": "Stream Info" }
}
}
| Part | ノート |
|---|---|
| object key | Required stable slug passed to the tab-content handler. 小文字のアルファベット、数字、およびハイフン、アルファベットから始まります。 |
title | 必須。 タブに表示されるラベル。 プラグインのタブ内で一意である必要があります。 |
content | オプション。 assets/内の静的HTMLファイルへの相対パス。 存在する場合、そのファイルのバイトが直接インライン化されます。 省略された場合、ホストはタブコンテンツハンドラを呼び出します。 |
The host derives the tab slug from the object key. Do not add a slug member to the value. The host rejects arrays and tab values containing the legacy slug member.
ui.modifyが必要です。 http.serve is not required: each static tab's HTML is read from assets/ and inlined into the tab body. For a dynamic tab, the host passes the object key to the tab-content handler as slug and inlines the returned HTML.
タブがどのように描画されるか
ホストは/api/configでpluginTabs[]配列を発行します。 ビューワーページは各エントリをタブにマッピングし、ボディはインライン化されたHTMLであり、ベースラインスタイルシートが注入されたサンドボックス化されたiframeに描画されるため、プレーンHTMLはあなた自身のCSSなしでネイティブに見えます。
See Styling plugin UI for the baseline and the helper classes. Tabs from each plugin are appended after the built-ins in lexicographic slug order. Ordering between tabs from different plugins is unspecified. JSON object order is not significant. The React key combines the tab slug and title, so changing either value remounts that tab.
The tab object key
The object key is a stable name you control. The host passes it to your tab-content handler as slug, so one handler can serve multiple tabs without guessing which one was requested. It also appears in host logs and future API calls, so pick something clear, like "music" or "stream-info". You can change title freely unless your code depends on it. Changing the key is a breaking change if code depends on the existing slug.
動的タブコンテンツ
When a tab value has no content file, the host calls your tab-content handler to produce it. ユーザーごとにコンテンツを変更する必要がある場合や、ライブデータを取得する場合にこれを実装します。 The host resolves every dynamic tab while building the viewer's /api/config payload, once per config request rather than on tab click, so keep the handler fast. It passes the tab's object key as slug with the viewer's identity, and expects the HTML string for the tab body:
- JavaScript
- Python
module.exports = definePlugin({
onTabContent(ctx) {
// ctx = { slug, user? }
if (ctx.slug === 'stream-info') {
return '<h2>Stream info</h2><p>Live every weekday at 8pm UTC.</p>';
}
return '';
},
});
@plugin.on_tab_content("stream-info")
def stream_info(ctx):
return "<h2>Stream info</h2><p>Live every weekday at 8pm UTC.</p>"
ハンドラ: タブのコンテンツを参照してペイロードの形状を確認してください。 ビューワーIDは認証されるときに存在し、匿名のビューワーには存在しません。
パスルール
extraPageContentと同様、エントリごとに適用されます:
"music.html"のようなベアパスは、あなたのプラグインの名前空間に自動的にプレフィックスが付けられます。- 完全修飾の
/plugins/\<your-slug>/...パスは通過します。 - 他のプラグインの名前空間にあるパスは拒否されます。
http(s)://URLは拒否されます。- 各エントリは
.htmlで終わる必要があります。
タブのタイトル
titleフィールドはタブバーにそのまま表示されます。 短く保ってください: 長いタイトルはタブUIによって切り捨てられます。 長さに関するスキーマの制約はありませんが、~16文字を超えるとモバイルでうまく収まりません。
タブを使用する時期とextraPageContentを使用する時期
extraPageContent: タブ行の上に配置される1つのHTMLブロック。 発表ストリップ、スポンサーのバナー、常に表示されるべきものに適しています。tabs: 視聴者がクリックする専用パネル。 チャットと競合する必要のないコンテンツに適しています: 音楽リスト、イベントのスケジュール、リンクページ、視聴者に見つけてもらいたいが、必ずしも最初に見させたくないスポンサーセクション。
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Gabe Kangas