Estamos migrando do sistema de renderização “widget” legado, substituindo-o por componentes Glimmer modernos.
Recentemente, modernizamos o fluxo de publicações (post stream) usando componentes Glimmer. Este guia o acompanhará na migração de seus plugins e temas do antigo sistema baseado em widgets para a nova implementação Glimmer.
Não se preocupe — essa migração é mais simples do que pode parecer à primeira vista. Projetamos o novo sistema para ser mais intuitivo e poderoso que o antigo sistema de widgets, e este guia o ajudará no processo.
Cronograma
Estas são estimativas sujeitas a alterações
Q2 2025:
Implementação principal concluída
Início da atualização dos componentes oficiais de plugins e temas
Habilitado no Meta
Publicação de orientações de atualização (este guia)
Q3 2025:
Conclusão da atualização dos componentes oficiais de plugins e temas
Definir glimmer_post_stream_modecomo padrãoautoe habilitar mensagens de descontinuação no console
Habilitar mensagens de descontinuação com um banner de aviso de administrador (planejado para julho de 2025)- Plugins e temas de terceiros devem ser atualizados
Q4 2025:
Habilitar o novo fluxo de publicações por padrão
Remover a configuração de feature flag e o código legado
O que isso significa para mim?
Se algum dos seus plugins ou temas utiliza alguma API de “widget” para personalizar o fluxo de publicações, você precisará atualizá-los para funcionar com a nova versão.
Como posso testar o novo fluxo de publicações?
Para testar o novo fluxo de publicações, basta alterar a configuração glimmer_post_stream_mode para auto nas configurações do seu site. Isso habilitará o novo fluxo de publicações, desde que você não tenha plugins ou temas incompatíveis.
Quando glimmer_post_stream_mode estiver definido como auto, o Discourse detectará automaticamente plugins e temas incompatíveis. Se encontrar algum, você verá mensagens de aviso úteis no console do navegador que identificam exatamente quais plugins ou temas precisam ser atualizados, juntamente com rastreamentos de pilha (stack traces) para ajudá-lo a localizar o código relevante.
Essas mensagens ajudarão você a identificar exatamente quais partes dos seus plugins ou temas precisam ser atualizadas para serem compatíveis com o novo fluxo de publicações Glimmer.
Se encontrar problemas ao usar o novo fluxo de publicações, não se preocupe: por enquanto, você ainda pode definir a configuração como disabled para retornar ao sistema antigo. Se tiver extensões incompatíveis instaladas, mas ainda quiser testar, como administrador, você pode definir a opção como enabled para forçar o novo fluxo de publicações. Use isso com cautela — seu site pode não funcionar corretamente, dependendo das personalizações que você possui.
Tenho plugins ou temas personalizados instalados. Preciso atualizá-los?
Você precisará atualizar seus plugins ou temas se realizarem alguma das seguintes personalizações:
-
Usar
decorateWidget,changeWidgetSetting,reopenWidgetouattachWidgetActionnestes widgets:actions-summaryavatar-flairembedded-postexpand-hiddenexpand-post-buttonfilter-jump-to-postfilter-show-allpost-articlepost-avatar-user-infopost-avatarpost-bodypost-contentspost-datepost-edits-indicatorpost-email-indicatorpost-gappost-group-requestpost-linkspost-locked-indicatorpost-meta-datapost-noticepost-placeholderpost-streampostposter-nameposter-name-titleposts-filtered-noticereply-to-tabselect-posttopic-post-visited-line
-
Usar um dos métodos de API abaixo:
addPostTransformCallbackincludePostAttributes
Se você tiver extensões que usam alguma das personalizações acima, verá um aviso no console identificando qual plugin ou componente precisa ser atualizado quando visitar uma página de tópico.
O ID de descontinuação é:
discourse.post-stream-widget-overrides
Se você usa mais de um tema em sua instância, verifique todos eles, pois os avisos aparecerão apenas para plugins ativos e temas e componentes de tema atualmente em uso.
Quais são as substituições?
O novo fluxo de publicações Glimmer oferece várias maneiras de personalizar como as publicações aparecem:
- Plugin Outlets: Para adicionar conteúdo em pontos específicos do fluxo de publicações.
- Transformers: Para personalizar itens, modificar estruturas de dados ou alterar o comportamento de componentes.
Substituindo includePostAttributes por addTrackedPostProperties
Se o seu plugin usa includePostAttributes para adicionar propriedades ao modelo de publicação, você precisará atualizá-lo para usar addTrackedPostProperties.
Antes:
api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
Depois:
api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
A função addTrackedPostProperties marca propriedades como rastreadas para atualizações de publicação. Isso é importante se o seu plugin adiciona propriedades a uma publicação e as usa durante a renderização. Isso garante que a interface do usuário seja atualizada automaticamente quando essas propriedades mudarem.
Padrões Comuns de Migração
Usando Plugin Outlets
Os Plugin Outlets são sua principal ferramenta para adicionar conteúdo em pontos específicos do fluxo de publicações. Pense neles como locais designados onde você pode injetar seu conteúdo personalizado.
Eles são a chave para migrar das decorações de widgets.
O Fluxo de Publicações Glimmer envolve conteúdo frequentemente personalizado em outlets. Use as funções da API de plugin renderBeforeWrapperOutlet e renderAfterWrapperOutlet para inserir conteúdo antes ou depois deles.
1. Substituindo decorações de widget por Plugin Outlets
A personalização mais comum é adicionar conteúdo às publicações. No sistema de widgets, você usaria decorateWidget. Com Glimmer, você usará Plugin Outlets.
Antes:
// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.post_number === 1 && post.topic.accepted_answer) {
return helper.attach("solved-accepted-answer", { post });
}
});
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizeWidgetPost(api);
});
}
};
Depois:
// Parte de um inicializador .gjs em um plugin
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... outras importações
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
/>
</template>
}
);
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizePost(api);
});
}
};
2. Adicionando conteúdo após o nome do autor
Se o seu plugin adiciona conteúdo após o nome do autor, você usará a API renderAfterWrapperOutlet com o outlet post-meta-data-poster-name.
Antes:
// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações
function customizeWidgetPost(api) {
api.decorateWidget(`poster-name:after`, (dec) => {
if (!isGPTBot(dec.attrs.user)) {
return;
}
return dec.widget.attach("persona-flair", {
personaName: dec.model?.topic?.ai_persona_name,
});
});
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizeWidgetPost(api);
});
}
};
Depois:
// Parte de um inicializador .gjs em um plugin
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-meta-data-poster-name",
class extends Component {
static shouldRender(args) {
return isGPTBot(args.post?.user);
}
<template>
<span class="persona-flair">{{@post.topic.ai_persona_name}}</span>
</template>
}
);
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizePost(api);
});
}
};
3. Adicionando conteúdo antes do conteúdo da publicação
// Parte de um inicializador .gjs em um tema
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações
function customizePost(api) {
api.renderBeforeWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.topic?.pinned;
}
<template>
<div class="pinned-post-notice">
Este é um tópico fixado
</div>
</template>
}
);
}
export default {
name: "pinned-topic-notice",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizePost(api);
});
}
};
4. Adicionando conteúdo após o conteúdo da publicação
// Parte de um inicializador .gjs em um tema
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.wiki;
}
// Em um componente real, você usaria um template assim:
<template>
<div class="wiki-post-notice">
Esta publicação é um wiki
</div>
</template>
}
);
}
export default {
name: "wiki-post-notice",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... outras personalizações
});
}
};
Usando Transformers
Transformers são uma maneira poderosa de personalizar componentes do Discourse. Eles permitem modificar dados ou o comportamento de componentes sem precisar substituir componentes inteiros.
Aqui estão alguns dos transformers de valor mais relevantes para personalização do fluxo de publicações:
| Nome do Transformer | Descrição | Contexto |
|---|---|---|
post-class |
Personalizar classes CSS aplicadas ao elemento principal da publicação. | { post } |
post-meta-data-infos |
Personalizar a lista de componentes de metadados exibidos para uma publicação. Isso permite adicionar, remover ou reordenar itens como a data da publicação, indicador de edição, etc. | { post, metaDataInfoKeys } |
post-meta-data-poster-name-suppress-similar-name |
Decidir se deve suprimir a exibição do nome completo do usuário quando for semelhante ao nome de usuário. Retorne true para suprimir. |
{ post, name } |
post-notice-component |
Personalizar ou substituir o componente usado para renderizar um aviso de publicação. | { post, type } |
post-show-topic-map |
Controlar a visibilidade do componente de mapa de tópicos na primeira publicação. | { post, isPM, isRegular, showWithoutReplies } |
post-small-action-class |
Adicionar classes CSS personalizadas a uma ação pequena de publicação. | { post, actionCode } |
post-small-action-custom-component |
Substituir uma ação pequena de publicação padrão por um componente Glimmer personalizado. | { post, actionCode } |
post-small-action-icon |
Personalizar o ícone usado para uma ação pequena de publicação. | { post, actionCode } |
poster-name-class |
Adicionar classes CSS personalizadas ao contêiner do nome do autor. | { user } |
1. Adicionando uma classe personalizada às publicações
// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostClasses(api) {
api.registerValueTransformer(
"post-class",
({ value, context }) => {
const { post } = context;
// Adicionar uma classe personalizada a publicações de um usuário específico
if (post.user_id === 1) {
return [...value, "special-user-post"];
}
return value;
}
);
}
export default {
name: "custom-post-classes",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizePostClasses(api);
});
}
};
2. Adicionando metadados personalizados à publicação
O transformer post-meta-data-infos permite adicionar componentes personalizados à seção de metadados da publicação.
// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostMetadata(api) {
// Definir um componente a ser usado na seção de metadados
// O componente deve ser criado fora do callback do transformer,
// caso contrário, pode causar problemas de memória.
const CustomMetadataComponent = <template>...</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
// Adicionar o componente apenas para publicações específicas
if (post.some_custom_property) {
metadata.add(
"custom-metadata-key",
CustomMetadataComponent,
{
// Posicioná-lo antes da data
before: metaDataInfoKeys.DATE,
// e após a aba de resposta
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
);
}
export default {
name: "custom-post-metadata",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizePostMetadata(api);
});
}
};
Aqui está um exemplo do mundo real do plugin discourse-activity-pub:
// Parte de um inicializador no plugin discourse-activity-pub
import { withPluginApi } from "discourse/lib/plugin-api";
import ActivityPubPostStatus from "../components/activity-pub-post-status";
import {
activityPubPostStatus,
showStatusToUser,
} from "../lib/activity-pub-utilities";
function customizePost(api, container) {
const currentUser = api.getCurrentUser();
const PostMetadataActivityPubStatus = <template>
<div class="post-info activity-pub">
<ActivityPubPostStatus @post={{@post}} />
</div>
</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
const site = container.lookup("service:site");
const siteSettings = container.lookup("service:site-settings");
if (
site.activity_pub_enabled &&
post.activity_pub_enabled &&
post.post_number !== 1 &&
showStatusToUser(currentUser, siteSettings)
) {
const status = activityPubPostStatus(post);
if (status) {
metadata.add(
"activity-pub-indicator",
PostMetadataActivityPubStatus,
{
before: metaDataInfoKeys.DATE,
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
}
);
}
export default {
name: "activity-pub",
initialize(container) {
withPluginApi((api) => {
customizePost(api, container);
// ... outras personalizações
});
}
};
5. Inserindo conteúdo antes ou depois do conteúdo formatado da publicação
Se o seu plugin adiciona conteúdo após o texto na publicação, você usará a API renderAfterWrapperOutlet com o outlet post-content-cooked-html.
Antes:
// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";
function customizeCooked(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.wiki) {
const banner = document.createElement("div");
banner.classList.add("wiki-footer");
banner.textContent = "Esta publicação é um wiki";
element.prepend(banner);
}
});
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
// ... outras personalizações
customizeCooked(api);
});
}
};
Depois:
// Parte de um inicializador em um plugin (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// Definir um componente a ser usado no plugin outlet
class WikiBanner extends Component {
static shouldRender(args) {
return args.post.wiki;
}
<template>
<div class="wiki-footer">Esta publicação é um wiki</div>
</template>
}
function customizePost(api) {
// Usar renderBeforeWrapperOutlet para adicionar conteúdo antes do conteúdo da publicação
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
WikiBanner
);
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... outras personalizações
});
}
};
Suportando Ambos os Sistemas Antigo e Novo Durante a Transição
Como autor de plugins ou temas, você pode querer suportar ambos os sistemas durante a transição para garantir que suas extensões funcionem com os fluxos de publicações antigo e novo.
Aqui está um padrão usado por muitos plugins oficiais:
// solved-button.js
import Component from "@glimmer/component";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { withPluginApi } from "discourse/lib/plugin-api";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
function customizePost(api) {
// personalizações do fluxo de publicações glimmer
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
@decoratorState={{@decoratorState}}
/>
</template>
}
);
// ...
// envolver o código antigo de widget silenciando os avisos de descontinuação
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}
// código antigo de widget
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
let post = helper.getModel();
if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
// Usar RenderGlimmer para renderizar um componente Glimmer no sistema de widgets
return new RenderGlimmer(
helper.widget,
"div",
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>
null,
{ post }
);
}
});
}
export default {
name: "extend-for-solved-button",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
if (siteSettings.solved_enabled) {
withPluginApi((api) => {
customizePost(api);
// ... outras personalizações
});
}
},
};
Exemplos do Mundo Real
Aqui estão links para pull requests reais de migração de nossos plugins oficiais. Eles mostram como atualizamos cada tipo de personalização:
- discourse-ai
- discourse-assign
- discourse-cakeday
- discourse-post-voting
- discourse-reactions
- discourse-shared-edits
- discourse-solved
- discourse-topic-voting
- discourse-user-notes
- discourse-activity-pub
Solução de Problemas
Meu site parece quebrado após habilitar o novo fluxo de publicações
- Defina
glimmer_post_stream_modede volta paradisabled - Verifique o console em busca de mensagens de erro específicas
- Atualize os plugins/temas problemáticos antes de tentar novamente
Não vejo nenhum aviso, mas minhas personalizações não estão funcionando
- Verifique se suas personalizações têm como alvo os widgets listados acima
- Certifique-se de estar testando na página de tópico com tópicos onde suas personalizações são visíveis
Precisa de Ajuda?
Se você encontrou um bug ou sua personalização não pode ser alcançada usando as novas APIs que introduzimos, por favor, informe-nos abaixo.