Ir para o conteúdo principal

Contributing web UI with Plugins

Plugins podem adicionar sua própria UI ao Owncast em dois lugares: abas dentro do admin (para configurações voltadas para o streamer) e botões de ação abaixo da transmissão (para ações voltadas para o visualizador). Ambos são declarados em seu manifesto e gerenciados pelo host. Você envia o conteúdo, o Owncast o encaixa no chrome correto.

As declarações de manifesto nesta página são JSON puro, idênticas a qualquer idioma que você escreva. Manipuladores de conteúdo dinâmico e chamadas em tempo de execução são mostradas para ambos os SDKs. Veja JavaScript ou Python para instalar e configurar.

Páginas de admin

Owncat suggestsSó precisa de um formulário de configurações?

Para configurações simples e tipadas (strings, números, switches), declare um bloco config no manifesto e deixe o Owncast renderizar o formulário para você. Veja Configuração. Crie uma página de admin personalizada quando precisar de uma UI que o formulário automático não pode expressar.

Plugins podem registrar páginas que aparecem dentro da UI de admin do Owncast sob Plugins. Declare them as an object keyed by plugin-relative path glob:

{
"permissions": ["http.serve"],
"admin": {
"pages": {
"/admin": { "title": "Settings", "icon": "gear" }
}
}
}

Each entry has:

PartNotas
object keyRequired path glob under /plugins/\<your-slug>/. Exemplos: "/admin", "/admin/*", "/admin/api/*".
titleRequired tab label inside the plugin's admin view.
iconOptional short semantic name. Suportado: gear, wrench, user, users, lock, info, apps, docs, bell (alias como settings e notifications também funcionam).

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.

Como são renderizados

O admin do Owncast renderiza cada página declarada como uma aba dentro de /admin/plugins/configure?id=\<seu-slug>. The tab body is an \<iframe> pointed at the path from the object key under /plugins/\<your-slug>/. Cada plugin recebe uma URL que pode ser marcada e uma entrada na barra lateral sob Plugins na navegação do admin.

Uma página de admin de plugin renderizada como uma aba dentro do admin, ao lado de suas abas de Instruções e Permissões

O host injeta automaticamente a folha de estilo base nas respostas HTML em caminhos de admin, de modo que controles simples <input> e \<button> parecem nativos à admin do Owncast sem que você precise enviar CSS. Veja Estilizando a UI do plugin para o que você recebe gratuitamente e as classes auxiliares disponíveis. Plugins que preferirem sua própria estilização podem adicionar em cima.

Sandbox

A página é executada em um \<iframe> isolado. Seus scripts são executados, formulários são enviados e fetch de mesma origem para seus próprios endpoints /plugins/\<seu-slug>/ funciona. As páginas também podem abrir popups, iniciar downloads de arquivos (por exemplo, um blob ou URL de dados \<a download> que você clica a partir do script) e usar diálogos confirm() / alert() / prompt(). O sandbox é a única restrição que você geralmente notará. Se um recurso do navegador parecer silenciosamente bloqueado, o sandbox do iframe é a primeira coisa a verificar.

Autenticação condicionante

Solicitações para caminhos de admin declarados no manifesto são autenticadas pelo host. Solicitações não autenticadas recebem um 401 antes que seu código de plugin seja executado. Você não precisa verificar a autenticação da solicitação para esses caminhos.

Arquivos estáticos e endpoints dinâmicos sob os caminhos correspondentes são ambos autenticados. A mesma autenticação se aplica ao seu public/admin/index.html e ao POST /admin/api/save-settings.

Use múltiplos globs quando você tiver tanto uma página de UI quanto uma API JSON:

{
"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.

Fluxo de autor

  1. Coloque HTML, CSS e JS de admin em public/admin/index.html (e amigos).
  2. Exponha APIs de admin através do seu manipulador de solicitações em /admin/api/... (veja Servindo HTTP).
  3. Declare the relevant path keys in manifest.admin.pages.
  4. Visite /admin/plugins/configure?id=\<seu-slug> na interface de admin. O Owncast usa seu login de admin existente para restringir a página. Sem prompt extra.

Botões de ação

O Owncast apresenta uma linha de botões de ação em sua UI de visualizador. Entradas clicáveis que abrem uma URL (em um modal ou nova aba) ou renderizam HTML bruto. Plugins podem contribuir com os seus próprios.

Uma linha de botões de ação contribuídos por plugins abaixo da transmissão na página do visualizador, ao lado dos botões de Seguir e Notificar embutidos

Botões declarados no manifesto

{
"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>"
}
]
}

Enquanto seu plugin estiver ativado, o host mescla suas entradas de ação na lista que o Owncast já exibe sob a transmissão. Quando desativados, eles desaparecem.

Referência de campo

CampoNotas
titleObrigatório. O rótulo do botão.
urlPode ser uma URL absoluta https://... ou um caminho. Mutualmente exclusivo com html.
htmlHTML bruto renderizado em um modal embutido. Mutualmente exclusivo com url.
iconURL da imagem opcional exibida no botão. Mesmas regras de caminho que url.
colorCor hexadecimal opcional para o fundo do botão.
descriptionOpcional. Exibido no modal que se abre para ações baseadas em URL.
openExternallySe true, a URL abre em uma nova aba em vez de um modal embutido.

Regras de caminho

Duas regras simples cobrem tudo:

  • Caminhos relativos se auto-prefixam ao namespace do seu plugin. "/" se torna /plugins/meu-plugin/. "/star.png" se torna /plugins/meu-plugin/star.png. Isso o impede de codificar o nome do seu plugin. Aplica-se tanto a url quanto a icon.
  • URLs absolutas https://... passam sem alterações. Use esses para links externos e ícones hospedados em CDN.

O host impõe:

  • Permissão ui.modify é necessária. Manifestos com actions mas sem ui.modify são rejeitados ao carregar.
  • Exatamente um de url ou html por entrada.
  • URLs e ícones que se resolvem em seu namespace requerem http.serve. Você é quem os serve.
  • URLs e ícones que apontam para o namespace de outro plugin são rejeitados. Captura erros de digitação e impede que um plugin anuncie a UI de outro.

Adições em tempo de execução

Um plugin pode adicionar mais botões de ação em tempo de execução, sem recarregar, chamando owncast.actions.add(...) com uma única ação ou um array delas. Cada entrada em tempo de execução passa pela mesma validação que manifest.actions, e é persistida na configuração do plugin, para que as adições sobrevivam a uma recarga. owncast.actions.clear() remove todas as adições em tempo de execução. As ações declaradas no manifesto permanecem.

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([ { ... }, { ... } ])
},
});

Um padrão comum é uma página de admin que permite ao streamer adicionar botões personalizados (rótulo + URL) por cima dos padrões do plugin. O exemplo de action-buttons no SDK fornece uma versão funcional disso.

Estilizando a UI do plugin

O Owncast injeta uma folha de estilo básica em cada superfície de plugin que renderiza em um iframe: suas páginas de admin e suas abas de página de visualizador. É construída com os próprios tokens de design do Owncast, então HTML semântico simples adota a aparência nativa sem CSS próprio.

  • Títulos, parágrafos e links adotam as fontes e cores do tema.
  • <input>, \<textarea>, \<select> e \<button> renderizam como os controles nativos. Um \<button> recebe o estilo primário. Adicione class="secondary" para a variante de contorno.
  • \<table>, \<fieldset> e \<code> / \<pre> recebem uma estilização nativa sensata.

Seu conteúdo fica ajustado na página. O fundo do iframe é transparente, então o painel do host aparece, da mesma forma que as aberturas embutidas de Sobre e Seguidores são renderizadas. Você não recebe, e não deve adicionar, um fundo de página opaco ou uma caixa de contorno ao redor de tudo. Essa renderização gradeada é o que faz uma aba de plugin parecer parte do Owncast, em vez de um iframe embutido.

Owncat informs youOnde se aplica

Os estilos básicos se aplicam às superfícies renderizadas em iframe: páginas de admin e abas de página de visualizador. Conteúdo que você injeta diretamente na página do visualizador (extraPageContent, scripts) é renderizado no DOM da página real e herda os estilos reais do Owncast.

Classes auxiliares

Para blocos de construção nativos além de elementos simples, a base envia algumas classes opcionais. Elas referenciam os mesmos tokens de tema que o resto do Owncast, então elas são reestilizadas automaticamente quando um admin personaliza o tema.

ClasseO que faz
cardUma superfície de cartão nativa, o mesmo visual que os cartões de seguidores e transmissões em destaque. Um \<section> / \<article> simples permanece ajustado, então opte por class="card" quando você quiser a superfície em caixa.
card interactiveAdicione interactive a um cartão clicável para o levantamento de hover nativo.
card-gridUma grade responsiva que preenche quantas colunas de ~260px couberem e colapsa em uma coluna em uma janela estreita. Coloque filhos card diretamente.
tagUma tag ou distintivo tipo pílula, correspondendo às tags nos cartões de transmissão nativos.
stackUma coluna flex vertical com um espaço consistente.
rowUma linha flex horizontal que se enrola, com um espaço consistente.
mutedTexto desfeito, para legendas e detalhes secundários.
<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>

Tudo aqui é opcional. Uma aba que entrega apenas HTML semântico já parece nativa. Use os auxiliares quando quiser cartões, grades ou tags sem precisar digitar os valores do Owncast, e adicione seu próprio CSS por cima (veja Folhas de estilo do visualizador) sempre que precisar de algo que a base não cobre.

Folhas de estilo do visualizador

Plugins podem tematizar a página do visualizador agrupando arquivos CSS e listando-os em manifest.styles. O host insere o conteúdo de cada arquivo em um único bloco de estilos de plugin na página, então os plugins estendem o CSS da página sem que cada contribuição precise de sua própria tag <link>.

{
"permissions": ["ui.modify"],
"styles": ["theme.css", "overrides.css"]
}

Requer apenas ui.modify (o plugin pinta dentro do chrome do Owncast). http.serve não é necessário: o host lê cada arquivo do diretório assets/ do seu plugin e insere os bytes no bloco de estilos de plugin na página em /api/config, não em uma URL.

Regras de caminho

  • Caminhos simples como "theme.css" se auto-prefixam para o namespace do seu plugin.
  • "/theme.css" resolve da mesma forma.
  • Caminhos totalmente qualificados /plugins/\<your-slug>/... passam por eles.
  • Caminhos no namespace de outro plugin são rejeitados.
  • URLs http:// e https:// são rejeitadas. Agregue ativos externos e faça referência a eles com @font-face ou url(...) a partir do seu CSS em vez disso, para que um administrador que revisar o manifesto veja cada arquivo que será carregado.
  • Cada entrada deve terminar com .css.

Como as contribuições são renderizadas

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. Desabilitar o plugin remove sua contribuição na próxima carga da página.

O corpo do CSS funciona contra o DOM do visualizador ao vivo, então seus seletores visam o que a página renderiza. Escopar cada regra sob um único id raiz é um hábito defensivo que vale a pena manter. Sem isso, suas regras podem corresponder a elementos que a página host renderiza e produzir regressões surpreendentes.

Onde os estilos do plugin se situam na cascata

A página do visualizador constrói sua aparência a partir de quatro camadas, aplicadas nesta ordem. Camadas posteriores ganham.

  1. Os padrões internos do Owncast.
  2. Estilos do plugin: seus arquivos manifest.styles primeiro, depois sua saída onPageStyles.
  3. As variáveis de aparência do administrador, as cores definidas com os seletores em Configurações Gerais → Aparência.
  4. O CSS personalizado do administrador, o editor na mesma página.

Seus estilos são a camada 2, então as escolhas explícitas do administrador nas camadas 3 e 4 sobrescrevem as suas em qualquer propriedade que vocês dois definirem. Trate um tema como uma base em vez da palavra final:

  • Um token que você configurou e que o administrador deixou como padrão mostra seu valor.
  • Um token que você configurou e que o administrador também configurou mostra o valor do administrador.

Tanto temas parciais quanto completos são válidos. Um plugin que apenas recolorir links deixa todas as outras cores inalteradas. Um plugin que define toda a paleta ainda cede a qualquer cor individual que o administrador escolher. O administrador permanece no controle de sua própria instância, e a página de Aparência informa a eles que um plugin está envolvido: mostra um aviso nomeando seu plugin e marca cada cor que você definiu com uma nota também definida por \<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.

Uma saída especial quebra a ordem: uma regra de plugin marcada como !important supera as declarações normais do administrador, independentemente da camada. Evite isso em CSS de tema se você quiser que o administrador mantenha a palavra final sobre suas cores.

Cuidado: URLs relativas em CSS

Referências url(...) dentro do CSS de um plugin são resolvidas contra a página do visualizador, não contra o namespace do plugin. Se você quiser fazer referência a uma imagem agregada, use o caminho absoluto /plugins/\<your-slug>/logo.png em vez de ./logo.png. O mesmo vale para as fontes @font-face. O espaço URL estático do plugin permanece disponível, então referências diretas funcionam mesmo que nenhum <link> aponte para o arquivo.

Folhas de estilo dinâmicas: onPageStyles

Quando o CSS depende do estado do plugin, um tema que o administrador selecionou ou um valor no armazenamento KV, retorne-o de um manipulador onPageStyles em vez de (ou junto com) um arquivo estático. Não há campo de manifesto para isso. O host chama o manipulador uma vez por /api/config para qualquer plugin que tenha ui.modify e o exporte, e depois anexa o que ele retorna ao seu bloco de estilos de plugin após os arquivos estáticos manifest.styles. Dentro dos próprios estilos do seu plugin, a regra posterior prevalece, então retornar apenas a sobrescrição ativa de onPageStyles é o suficiente. O bloco todo ainda fica abaixo das configurações de aparência do administrador (veja onde os estilos do plugin se situam na cascata).

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}; }`;
},
});

Requer ui.modify. The examples above also read the KV store, which separately requires storage.kv. Retorne nada (um return vazio, o mesmo que retornar "") quando não houver nada a contribuir em um determinado pedido. A chamada não leva argumentos por visualizador, então a resposta /api/config permanece cacheável. O exemplo theme-hub no SDK usa isso para aplicar um tema selecionado pelo administrador a toda a interface do visualizador.

Scripts do visualizador

Plugins podem estender o tempo de execução da página do visualizador juntando arquivos JavaScript e listando-os em manifest.scripts. O conteúdo de cada arquivo é anexado à resposta /customjavascript que o Owncast já serve para o JS personalizado do administrador, então os plugins estendem o comportamento da página sem que cada contribuição precise de sua própria tag \<script>.

{
"permissions": ["ui.modify"],
"scripts": ["client.js"]
}

As mesmas regras de permissão e caminho do styles, aplicadas a arquivos .js (apenas ui.modify é necessário, e o host lê de assets/ e insere na /customjavascript). Cada contribuição é prefixada com um comentário // plugin: \<your-slug> ... .

Estes são scripts da página do visualizador que rodam no navegador, sempre JavaScript, independentemente de qual linguagem você escreveu o plugin no lado do servidor.

Contexto de execução

A página do visualizador carrega /customjavascript como uma única tag \<script async>. O JS de cada plugin roda na mesma janela global que o JS personalizado do administrador e o restante do chrome do Owncast. Três implicações:

  • Declarações de var e function no nível superior vão para 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.
  • O host envolve a contribuição de cada plugin em seu próprio try/catch, de modo que um erro em tempo de execução jogue para o console do navegador (prefixado owncast plugin \<your-slug> script error:) sem parar os scripts dos outros plugins. Um erro de sintaxe não é isolado: ele quebra a análise da única tag de script concatenada antes de qualquer try/catch ser executado, então envie JavaScript válido.
  • URLs relativas como fetch('./data.json') são resolvidas em relação à URL da página do visualizador, não em relação ao seu plugin. Use caminhos absolutos como /plugins/\<your-slug>/data.json para arquivos que você envia em public/.

Scripts dinâmicos: onPageScripts

O contraponto do script para onPageStyles. Retorne JavaScript calculado no tempo do pedido de um manipulador onPageScripts, sem campo de manifesto. O host o chama uma vez por /api/config para qualquer plugin que tenha ui.modify que o exporte, e anexa o resultado a /customjavascript após os arquivos estáticos de manifest.scripts, envoltos no mesmo try/catch por plugin.

Isto é para qualquer JavaScript relacionado ao tempo de pedido, não apenas para temas. Use-o para executar código do lado do visualizador que foi computado por pedido, por exemplo, exibindo um valor que o administrador definiu no armazenamento KV do plugin. O exemplo abaixo mostra esse valor para os visualizadores:

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)});`;
},
});

A saída roda na window compartilhada do visualizador, então o conselho sobre IIFE e caminhos absolutos ainda se aplica. Escape quaisquer strings não confiáveis que você embutir: JSON.stringify em JavaScript e json.dumps em Python produzem um literal adequadamente citado, que é por isso que os exemplos a envolvem antes de passá-las para alert. Like the styles examples, reading the KV store requires storage.kv on top of ui.modify. Retorne nada (um return vazio, o mesmo que retornar "") para não contribuir com nada.

Quando usar

scripts é a ferramenta certa para plugins que precisam reagir ao estado do lado do visualizador, montar sua própria interface sobre a página ou se comunicar com um backend que o plugin roda em /plugins/\<your-slug>/. Para bots dirigidos por chat, filtros de mensagens e qualquer lógica que deve rodar do lado do servidor, os manipuladores de plugin regulares são uma melhor opção. Eles rodam dentro da sandbox do host, podem se comunicar com APIs do Owncast que a página do visualizador não pode acessar, e não confiam no DOM controlado pelo usuário.

Conteúdo extra da página

Plugins podem adicionar HTML à seção de conteúdo extra da página do visualizador. Declare manifest.extraPageContent como um objeto com um slug obrigatório e um caminho content opcional:

{
"permissions": ["ui.modify"],
"extraPageContent": { "slug": "banner", "content": "content.html" }
}
CampoNotas
slugObrigatório. Um identificador estável passado para o manipulador de conteúdo da página quando o host pede HTML renderizado. Letras minúsculas, dígitos e hífens, começando com uma letra.
contentOpcional. Caminho relativo para um arquivo HTML estático em assets/. Quando presente, os bytes desse arquivo são inseridos diretamente. Quando omitido, o host chama seu manipulador de conteúdo da página em vez disso.

Estático vs dinâmico

Use content quando o HTML for o mesmo para todos os visualizadores: tiras de anúncios, banners de patrocinadores, blocos de prosa. Deixe content de fora e implemente um manipulador de conteúdo da página quando o conteúdo deve mudar por visualizador ou depender de dados ao vivo. O host chama o manipulador com o slug solicitado e a identidade do visualizador, e seu manipulador retorna a string HTML a ser renderizada:

module.exports = definePlugin({
onPageContent(ctx) {
if (ctx.slug === 'banner') {
const who = ctx.user ? `, ${ctx.user.displayName}` : '';
return `<div class="banner">Welcome${who}!</div>`;
}
return '';
},
});

Veja Manipuladores: conteúdo da página para a forma do payload. A identidade do visualizador está presente quando o visualizador está autenticado e ausente para visualizadores anônimos.

Requer ui.modify. http.serve não é necessário: o HTML é inserido na resposta /api/config, não servido como uma URL.

Os bytes aparecem no topo do bloco de conteúdo extra, acima de qualquer prosa que o administrador tenha configurado. Each contribution is wrapped with an <!-- plugin: <your-slug> ... --> comment for attribution. As contribuições de múltiplos plugins se empilham na ordem em que o host as carregou.

Regras de caminho

As mesmas que styles e scripts, aplicadas a uma única entrada .html. Um arquivo por plugin. Se você quiser vários blocos distintos, vincule ou \<iframe> eles a partir do único arquivo que você envia.

Markdown vs HTML

O conteúdo extra da página do administrador passa pelo processador de markdown do Owncast antes de ser renderizado. O HTML do plugin não: o host executa o processador de markdown primeiro no conteúdo do administrador, e então acrescenta seus bytes brutos. Tags, atributos e scripts inline passam como escritos.

Isso significa que o HTML do plugin pode usar qualquer elemento que a página do visualizador aceite. Isso também significa que uma tag malformada pode quebrar o chrome circundante, então escape qualquer string não confiável que você embutir (nomes de usuários, texto buscado, qualquer coisa que não esteja sob seu controle).

Emparelhando com scripts

extraPageContent brilha quando emparelhado com scripts: envie a marcação como HTML onde possa ser revisada facilmente, e conecte interações de seu JavaScript consultando os elementos que você declarou. O host carrega o HTML antes que o script execute, então um script direcionado para document.getElementById(...) em um elemento contribuído pelo plugin funciona sem truques de tempo.

{
"permissions": ["ui.modify", "http.serve"],
"extraPageContent": { "slug": "panel", "content": "panel.html" },
"scripts": ["panel.js"]
}

Um padrão que muitas vezes lê mais limpo do que construir o mesmo DOM imperativamente de um plugin só de scripts:

  • panel.html declara estrutura, classes e IDs que você pode entender como HTML simples.
  • panel.css (declarado em styles) faz o tema.
  • panel.js anexa ouvintes de eventos, busca dados, muta estado.

Quando usar HTML mais JS em vez de JavaScript puro: qualquer coisa com layout não trivial, atributos ARIA ou widgets de terceiros que esperam serem inicializados a partir do DOM existente. Pure scripts still makes sense for plugins that build their UI only on certain conditions (after a fetch, after a user action) where rendering nothing on initial paint is the right behavior.

Quando extraPageContent é suficiente por si só

Standalone, extraPageContent é o caminho mais simples para tiras de anúncio, banners de patrocinadores e qualquer bloco que não precise reagir a eventos: ele envia a marcação diretamente, não requer um script e sobrevive a um visualizador que desativou o JavaScript.

Abas da página do visualizador

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" }
}
}
PartNotas
object keyRequired stable slug passed to the tab-content handler. Letras minúsculas, dígitos e hífens, começando com uma letra.
titleRequerido. O rótulo exibido na aba. Deve ser exclusivo dentro das abas do plugin.
contentOpcional. Caminho relativo para um arquivo HTML estático em assets/. Quando presente, os bytes desse arquivo são embutidos diretamente. Quando omitido, o host chama o manipulador de conteúdo da aba em vez disso.

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.

Requer 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.

Como abas são renderizadas

O host emite um array pluginTabs[] em /api/config. A página do visualizador mapeia cada entrada para uma aba cujo corpo é o HTML embutido, renderizado em um iframe isolado com a folha de estilo padrão injetada, de modo que HTML simples pareça nativo sem seu próprio CSS.

Plugin-contributed tabs on the viewer page, shown alongside the built-in About and Followers tabs

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.

Conteúdo dinâmico de aba

When a tab value has no content file, the host calls your tab-content handler to produce it. Implemente-o quando o conteúdo deve mudar por visualizador ou puxar dados ao vivo. 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:

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 '';
},
});

Veja Manipuladores: conteúdo da aba para a forma da carga. A identidade do visualizador está presente quando autenticada e ausente para visualizadores anônimos.

Regras de caminho

Assim como extraPageContent, aplicado por entrada:

  • Caminhos simples como "music.html" são auto-prefixados para o namespace do seu plugin.
  • Caminhos totalmente qualificados /plugins/\<seu-slug>/... passam.
  • Caminhos em outro namespace de plugin são rejeitados.
  • URLs http(s):// são rejeitadas.
  • Cada entrada deve terminar em .html.

Título da aba

O campo title aparece verbalmente na barra de abas. Mantenha-o curto: títulos longos são cortados pela interface da aba. Não há restrição de esquema sobre o comprimento, mas qualquer coisa acima de ~16 caracteres não ficará bem em dispositivos móveis.

Quando usar abas vs extraPageContent

  • extraPageContent: um bloco de HTML que fica acima da linha de abas. Bom para tiras de anúncio, banners de patrocinadores, qualquer coisa que deva estar sempre visível.
  • tabs: painéis dedicados em que o visualizador clica. Bom para conteúdo que não precisa competir com o chat pela atenção: listas de músicas, horários de eventos, páginas de links, seções de patrocinadores que você deseja que os visualizadores encontrem, mas não necessariamente vejam primeiro.

Improve this page

See something missing or incorrect? Edit this page and improve the documentation for everyone.

Contributors to this documentation
Gabe KangasGabe Kangas
G
Gabe Kangas