Estamos abandonando el sistema de renderizado heredado de “widgets” para reemplazarlo con componentes modernos de Glimmer.
Recientemente hemos modernizado el flujo de publicaciones (post stream) utilizando componentes de Glimmer. Esta guía te acompañará en la migración de tus plugins y temas desde el antiguo sistema basado en widgets hacia la nueva implementación de Glimmer.
No te preocupes: esta migración es más sencilla de lo que podría parecer a primera vista. Hemos diseñado el nuevo sistema para que sea más intuitivo y potente que el antiguo sistema de widgets, y esta guía te ayudará a través del proceso.
Cronograma
Estas son estimaciones sujetas a cambios
Q2 2025:
Finalización de la implementación central
Inicio de la actualización de los componentes oficiales de plugins y temas
Activado en Meta
Publicación de consejos de actualización (esta guía)
Q3 2025:
Finalización de la actualización de los componentes oficiales de plugins y temas
Establecer glimmer_post_stream_modepor defecto enautoy activar mensajes de obsolescencia en la consola
Activar mensajes de obsolescencia con un banner de advertencia para administradores (planificado para julio de 2025)- Los plugins y temas de terceros deben actualizarse
Q4 2025:
Activar el nuevo flujo de publicaciones por defecto
Eliminar la configuración de la bandera de función y el código heredado
¿Qué significa esto para mí?
Si alguno de tus plugins o temas utiliza cualquier API de “widget” para personalizar el flujo de publicaciones, deberás actualizarlos para que funcionen con la nueva versión.
¿Cómo puedo probar el nuevo flujo de publicaciones?
Para probar el nuevo flujo de publicaciones, simplemente cambia la configuración glimmer_post_stream_mode a auto en la configuración de tu sitio. Esto activará el nuevo flujo de publicaciones si no tienes plugins o temas incompatibles.
Cuando glimmer_post_stream_mode está configurado en auto, Discourse detecta automáticamente los plugins y temas incompatibles. Si encuentra alguno, verás mensajes de advertencia útiles en la consola del navegador que identifican exactamente qué plugins o temas necesitan actualizarse, junto con trazas de pila (stack traces) para ayudarte a localizar el código relevante.
Estos mensajes te ayudarán a identificar exactamente qué partes de tus plugins o temas deben actualizarse para ser compatibles con el nuevo flujo de publicaciones de Glimmer.
Si encuentras algún problema al usar el nuevo flujo de publicaciones, no te preocupes; por ahora, puedes seguir configurando la opción en disabled para volver al sistema antiguo. Si tienes extensiones incompatibles instaladas pero aún quieres probarlo, como administrador puedes establecer la opción en enabled para forzar el nuevo flujo de publicaciones. Úsalo con precaución: es posible que tu sitio no funcione correctamente dependiendo de las personalizaciones que tengas.
Tengo plugins o temas personalizados instalados. ¿Necesito actualizarlos?
Deberás actualizar tus plugins o temas si realizan alguna de las siguientes personalizaciones:
-
Utilizan
decorateWidget,changeWidgetSetting,reopenWidgetoattachWidgetActionen estos 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
-
Utilizan uno de los siguientes métodos de la API:
addPostTransformCallbackincludePostAttributes
Si tienes extensiones que utilizan alguna de las personalizaciones anteriores, verás una advertencia en la consola que identifica qué plugin o componente necesita actualizarse cuando visites una página de tema.
El ID de obsolescencia es:
discourse.post-stream-widget-overrides
Si utilizas más de un tema en tu instancia, asegúrate de revisar todos ellos, ya que las advertencias solo aparecerán para los plugins activos y los temas y componentes de temas actualmente en uso.
¿Cuáles son los reemplazos?
El nuevo flujo de publicaciones de Glimmer te ofrece varias formas de personalizar cómo aparecen las publicaciones:
- Plugin Outlets (Salidas de Plugins): Para agregar contenido en puntos específicos del flujo de publicaciones.
- Transformers (Transformadores): Para personalizar elementos, modificar estructuras de datos o cambiar el comportamiento de los componentes.
Reemplazar includePostAttributes con addTrackedPostProperties
Si tu plugin utiliza includePostAttributes para agregar propiedades al modelo de publicación, deberás actualizarlo para usar addTrackedPostProperties en su lugar.
Antes:
api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
Después:
api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
La función addTrackedPostProperties marca las propiedades como rastreadas para las actualizaciones de publicaciones. Esto es importante si tu plugin agrega propiedades a una publicación y las utiliza durante el renderizado. Asegura que la interfaz de usuario se actualice automáticamente cuando estas propiedades cambien.
Patrones Comunes de Migración
Usando Plugin Outlets
Las salidas de plugins (Plugin Outlets) son tu herramienta principal para agregar contenido en puntos específicos del flujo de publicaciones. Piensa en ellas como espacios designados donde puedes inyectar tu contenido personalizado.
Son la clave para migrar desde las decoraciones de widgets.
El flujo de publicaciones de Glimmer envuelve a menudo el contenido personalizado en salidas. Utiliza las funciones de la API de plugins renderBeforeWrapperOutlet y renderAfterWrapperOutlet para insertar contenido antes o después de ellas.
1. Reemplazar decoraciones de widgets con Plugin Outlets
La personalización más común es agregar contenido a las publicaciones. En el sistema de widgets, usarías decorateWidget. Con Glimmer, utilizarás salidas de plugins en su lugar.
Antes:
// Parte de un inicializador en un plugin
import { withPluginApi } from "discourse/lib/plugin-api";
// ... otras importaciones
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) => {
// ... otras personalizaciones
customizeWidgetPost(api);
});
}
};
Después:
// Parte de un inicializador .gjs en un plugin
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... otras importaciones
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) => {
// ... otras personalizaciones
customizePost(api);
});
}
};
2. Agregar contenido después del Nombre del Autor
Si tu plugin agrega contenido después del nombre del autor, utilizarás la API renderAfterWrapperOutlet con la salida post-meta-data-poster-name.
Antes:
// Parte de un inicializador en un plugin
import { withPluginApi } from "discourse/lib/plugin-api";
// ... otras importaciones
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) => {
// ... otras personalizaciones
customizeWidgetPost(api);
});
}
};
Después:
// Parte de un inicializador .gjs en un plugin
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... otras importaciones
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) => {
// ... otras personalizaciones
customizePost(api);
});
}
};
3. Agregar contenido antes del contenido de la publicación
// Parte de un inicializador .gjs en un tema
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... otras importaciones
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 es un tema fijado
</div>
</template>
}
);
}
export default {
name: "pinned-topic-notice",
initialize() {
withPluginApi((api) => {
// ... otras personalizaciones
customizePost(api);
});
}
};
4. Agregar contenido después del contenido de la publicación
// Parte de un inicializador .gjs en un tema
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... otras importaciones
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.wiki;
}
// En un componente real, usarías una plantilla como esta:
<template>
<div class="wiki-post-notice">
Esta publicación es un wiki
</div>
</template>
}
);
}
export default {
name: "wiki-post-notice",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... otras personalizaciones
});
}
};
Usando Transformers (Transformadores)
Los transformadores son una forma poderosa de personalizar los componentes de Discourse. Te permiten modificar datos o el comportamiento de los componentes sin tener que sobrescribir componentes enteros.
Aquí hay algunos de los transformadores de valores más relevantes para la personalización del flujo de publicaciones:
| Nombre del Transformador | Descripción | Contexto |
|---|---|---|
post-class |
Personalizar las clases CSS aplicadas al elemento principal de la publicación. | { post } |
post-meta-data-infos |
Personalizar la lista de componentes de metadatos mostrados para una publicación. Esto permite agregar, eliminar o reordenar elementos como la fecha de la publicación, el indicador de edición, etc. | { post, metaDataInfoKeys } |
post-meta-data-poster-name-suppress-similar-name |
Decidir si suprimir la visualización del nombre completo del usuario cuando es similar a su nombre de usuario. Devuelve true para suprimir. |
{ post, name } |
post-notice-component |
Personalizar o reemplazar el componente utilizado para renderizar un aviso de publicación. | { post, type } |
post-show-topic-map |
Controlar la visibilidad del componente del mapa de temas en la primera publicación. | { post, isPM, isRegular, showWithoutReplies } |
post-small-action-class |
Agregar clases CSS personalizadas a una publicación de acción pequeña. | { post, actionCode } |
post-small-action-custom-component |
Reemplazar una publicación de acción estándar con un componente Glimmer personalizado. | { post, actionCode } |
post-small-action-icon |
Personalizar el icono utilizado para una publicación de acción pequeña. | { post, actionCode } |
poster-name-class |
Agregar clases CSS personalizadas al contenedor del nombre del autor. | { user } |
1. Agregar una clase personalizada a las publicaciones
// Parte de un inicializador en un plugin
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostClasses(api) {
api.registerValueTransformer(
"post-class",
({ value, context }) => {
const { post } = context;
// Agregar una clase personalizada a publicaciones de un usuario específico
if (post.user_id === 1) {
return [...value, "special-user-post"];
}
return value;
}
);
}
export default {
name: "custom-post-classes",
initialize() {
withPluginApi((api) => {
// ... otras personalizaciones
customizePostClasses(api);
});
}
};
2. Agregar metadatos personalizados a la publicación
El transformador post-meta-data-infos te permite agregar componentes personalizados a la sección de metadatos de la publicación.
// Parte de un inicializador en un plugin
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostMetadata(api) {
// Definir un componente que se usará en la sección de metadatos
// El componente debe crearse fuera de la función de retorno del transformador,
// de lo contrario puede causar problemas de memoria.
const CustomMetadataComponent = <template>...</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
// Solo agregar el componente para publicaciones específicas
if (post.some_custom_property) {
metadata.add(
"custom-metadata-key",
CustomMetadataComponent,
{
// Posicionarlo antes de la fecha
before: metaDataInfoKeys.DATE,
// y después de la pestaña de respuesta
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
);
}
export default {
name: "custom-post-metadata",
initialize() {
withPluginApi((api) => {
// ... otras personalizaciones
customizePostMetadata(api);
});
}
};
Aquí hay un ejemplo del mundo real del plugin discourse-activity-pub:
// Parte de un inicializador en el 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);
// ... otras personalizaciones
});
}
};
5. Insertar contenido antes o después del contenido cocinado (cooked) de la publicación
Si tu plugin agrega contenido después del texto en la publicación, utilizarás la API renderAfterWrapperOutlet con la salida post-content-cooked-html.
Antes:
// Parte de un inicializador en un 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 publicación es un wiki";
element.prepend(banner);
}
});
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
// ... otras personalizaciones
customizeCooked(api);
});
}
};
Después:
// Parte de un inicializador en un plugin (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// Definir un componente que se usará en la salida del plugin
class WikiBanner extends Component {
static shouldRender(args) {
return args.post.wiki;
}
<template>
<div class="wiki-footer">Esta publicación es un wiki</div>
</template>
}
function customizePost(api) {
// Usar renderBeforeWrapperOutlet para agregar contenido antes del contenido de la publicación
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
WikiBanner
);
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... otras personalizaciones
});
}
};
Soporte para ambos sistemas antiguos y nuevos durante la transición
Como autor de plugins o temas, es posible que desees soportar ambos sistemas durante la transición para asegurar que tus extensiones funcionen tanto con el flujo de publicaciones antiguo como con el nuevo.
Aquí hay un patrón utilizado por muchos plugins oficiales:
// 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) {
// personalizaciones del flujo de publicaciones 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 el código antiguo de widgets silenciando las advertencias de obsolescencia
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}
// código antiguo de widgets
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 un componente Glimmer en el 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);
// ... otras personalizaciones
});
}
},
};
Ejemplos del mundo real
Aquí hay enlaces a solicitudes de extracción (pull requests) reales de migración de nuestros plugins oficiales. Estos muestran cómo actualizamos cada tipo de personalización:
- 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
Solución de problemas
Mi sitio se ve roto después de activar el nuevo flujo de publicaciones
- Configura
glimmer_post_stream_modede nuevo adisabled - Revisa la consola en busca de mensajes de error específicos
- Actualiza los plugins/temas problemáticos antes de intentarlo de nuevo
No veo ninguna advertencia, pero mis personalizaciones no funcionan
- Verifica que tus personalizaciones apunten a los widgets listados arriba
- Asegúrate de estar probando en la página de temas con temas donde tus personalizaciones sean visibles
¿Necesitas ayuda?
Si encontraste un error o tu personalización no puede lograrse utilizando las nuevas APIs que hemos introducido, por favor háznoslo saber a continuación.