Próximos cambios en la transmisión de publicaciones - Cómo preparar themes y plugins

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:

  • :white_check_mark: Finalización de la implementación central
  • :white_check_mark: Inicio de la actualización de los componentes oficiales de plugins y temas
  • :white_check_mark: Activado en Meta
  • :white_check_mark: Publicación de consejos de actualización (esta guía)

Q3 2025:

  • :white_check_mark: Finalización de la actualización de los componentes oficiales de plugins y temas
  • :white_check_mark: Establecer glimmer_post_stream_mode por defecto en auto y activar mensajes de obsolescencia en la consola
  • :white_check_mark: 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:

  • :white_check_mark: Activar el nuevo flujo de publicaciones por defecto
  • :white_check_mark: 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, reopenWidget o attachWidgetAction en estos widgets:

    • actions-summary
    • avatar-flair
    • embedded-post
    • expand-hidden
    • expand-post-button
    • filter-jump-to-post
    • filter-show-all
    • post-article
    • post-avatar-user-info
    • post-avatar
    • post-body
    • post-contents
    • post-date
    • post-edits-indicator
    • post-email-indicator
    • post-gap
    • post-group-request
    • post-links
    • post-locked-indicator
    • post-meta-data
    • post-notice
    • post-placeholder
    • post-stream
    • post
    • poster-name
    • poster-name-title
    • posts-filtered-notice
    • reply-to-tab
    • select-post
    • topic-post-visited-line
  • Utilizan uno de los siguientes métodos de la API:

    • addPostTransformCallback
    • includePostAttributes

:light_bulb: 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

:warning: 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:

  1. Plugin Outlets (Salidas de Plugins): Para agregar contenido en puntos específicos del flujo de publicaciones.
  2. 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:

Solución de problemas

Mi sitio se ve roto después de activar el nuevo flujo de publicaciones

  • Configura glimmer_post_stream_mode de nuevo a disabled
  • 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.

La transmisión Glimmer Post está habilitada en Meta.

Si descubre algún problema, publíquelo a continuación.

Al actualizar mis componentes, me encontré con un código extraño:

En primer lugar, me desconcierta un poco @user={{@post}}. ¿Podría ser un error tipográfico?

En segundo lugar, ¿por qué el PluginOutlet llamado post-avatar-flair y el UserAvatarFlair son elementos separados? Además, ¿por qué post-avatar-flair no es un contenedor como otros outlets cercanos?

Está funcionando, así que no creo que sea un error tipográfico. ¿Quizás en esta ubicación, el objeto post tiene todos los atributos que el componente UserAvatarFlair espera encontrar en el argumento @user? ¡Estoy de acuerdo en que se ve raro!

Basándome en la descripción del PR, parece que esas cosas se hicieron para mantener la coherencia con otros outlets ‘avatar-flair’ similares (que probablemente se introdujeron antes de que los ‘contenedores de outlets de plugins’ fueran algo).

Hemos fusionado una solicitud de extracción que habilita el Glimmer Post Stream por defecto.

Después de la próxima actualización, los sitios compatibles usarán automáticamente el nuevo Post Stream. Los sitios que usen extensiones incompatibles volverán a la versión anterior y mostrarán advertencias en la consola del navegador.

Por ahora, puedes forzar manualmente el uso del antiguo Post Stream estableciendo glimmer_post_stream_mode en disabled.

Si encuentras algún problema, por favor repórtalo a continuación.

Creo que recibo una advertencia debido a este código que luego enlaza a este hilo:

api.changeWidgetSetting('post-avatar', 'size', '120');

¿Cómo actualizaría eso para el nuevo sistema?

Algo como esto:

api.registerValueTransformer("post-avatar-size", () => {
    return "120";
});

Genial, gracias.

Creo que eso funciona. Puse ese código y luego intenté activar temporalmente todas las configuraciones de fuerza para el modo brillo y pareció que el tamaño del avatar y la lista de publicaciones funcionaron bien en mi sitio después de eso. Después de probar, desactivé las anulaciones ya que parece que no se recomiendan para su uso todavía. Pero mi sitio ahora debería estar listo para el cambio.

Hola @Boost. :smiley:

¿Qué quieres decir cuando dices que las anulaciones aún no se recomiendan para su uso? Si tu sitio está listo, se cambiará automáticamente a la transmisión de publicaciones de Glimmer.

Cuando actualices tu instalación de Discourse la próxima vez, el Glimmer Post Stream estará habilitado por defecto, incluso para sitios que todavía tengan personalizaciones incompatibles.

De hecho, todos los sistemas de renderizado de widgets ahora están deshabilitados, por lo que ninguna personalización basada en widgets se renderizará más.

Por ahora, en sitios incompatibles, el administrador puede volver a habilitar el comportamiento anterior modificando los valores de las siguientes configuraciones:

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

Para volver a habilitar el Glimmer Post Stream, ambas configuraciones deben cambiarse.

Esta es la fase final antes de eliminar el código antiguo de la base de código de Discourse, lo que se espera que ocurra en aproximadamente un mes. Posteriormente, ya no será posible volver a habilitar los widgets.

La opción en la configuración experimental advierte claramente contra su uso:

Habilita la nueva implementación de la transmisión de publicaciones ‘glimmer’ en modo ‘auto’ para los grupos de usuarios especificados. Esta implementación está en desarrollo activo y no está pensada para uso en producción. No desarrolles temas/plugins contra ella hasta que la implementación se finalice y se anuncie.

Por lo tanto, la redacción es bastante contundente al decir que “no está pensada para uso en producción. No desarrolles temas/plugins contra ella”.

Esa es la explicación de la opción Modo de transmisión de publicaciones Glimmer para grupos automáticos.

Buena observación. Se nos pasó por alto.

El Glimmer Post Stream es ahora el predeterminado, y la versión del widget se considera obsoleta y ya está programada para su eliminación. Actualizaré la descripción de la configuración.

El PR que elimina la publicación de widget después de la transmisión fue fusionado.