Changements à venir dans le flux de publication - Comment préparer les thèmes et les plugins

We’re moving away from the legacy “widget” rendering system, replacing it with modern Glimmer components.

We’ve recently modernized the post stream using Glimmer components. This guide will walk you through migrating your plugins and themes from the old widget-based system to the new Glimmer implementation.

Don’t worry—this migration is more straightforward than it might seem at first glance. We’ve designed the new system to be more intuitive and powerful than the old widget system, and this guide will help you through the process.

Timeline

These are estimates subject to change

Q2 2025:

  • :white_check_mark: Core implementation finished
  • :white_check_mark: Start upgrading the official plugins and theme components
  • :white_check_mark: Enabled on Meta
  • :white_check_mark: Published upgrade advice (this guide)

Q3 2025:

  • :white_check_mark: Finish upgrading the official plugins and theme components
  • :white_check_mark: Set glimmer_post_stream_mode default to auto and enable console deprecation messages
  • :white_check_mark: Enable deprecation messages with an admin warning banner (planned for July 2025)
  • Third-party plugins and themes should be updated

Q4 2025:

  • :white_check_mark: Enable the new post stream by default
  • 17th November: Remove the feature flag setting and legacy code

What does it mean for me?

If any of your plugins or themes use any ‘widget’ APIs to customize the post stream, you’ll need to update them to work with the new version.

How do I try the new post stream?

To try the new post stream, simply change the glimmer_post_stream_mode setting to auto in your site settings. This will enable the new post stream if you don’t have any incompatible plugins or themes.

When glimmer_post_stream_mode is set to auto, Discourse automatically detects incompatible plugins and themes and if it finds any, you’ll see helpful warning messages in the browser console that identify exactly which plugins or themes need updating, along with stack traces to help you locate the relevant code.

These messages will help you identify exactly which parts of your plugins or themes need to be updated to be compatible with the new Glimmer post stream.

If you encounter any issues using the new post stream, don’t worry, for now, you can still set the setting to disabled to return to the old system. If you have incompatible extensions installed, but still want to try it, as an admin you can set the option to enabled to force the new post stream. Just use this with caution—your site might not work properly depending on which customizations you have.

I have custom plugins or themes installed. Do I need to update them?

You’ll need to update your plugins or themes if they perform any of the following customizations:

  • Use decorateWidget, changeWidgetSetting, reopenWidget or attachWidgetAction on these 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
  • Use one of the API methods below:

    • addPostTransformCallback
    • includePostAttributes

:light_bulb: If you have extensions that use any of the customizations above, you’ll see a warning in the console identifying which plugin or component needs to be upgraded when you visit a topic page.

The deprecation ID is: discourse.post-stream-widget-overrides

:warning: If you use more than one theme in your instance, be sure to check all of them since the warnings will only appear for active plugins and currently used themes and theme-components.

What are the replacements?

The new Glimmer post stream gives you several ways to customize how posts appear:

  1. Plugin Outlets: For adding content at specific points in the post stream.
  2. Transformers: For customizing items, modifying data structures, or changing the behavior of components.

Replacing includePostAttributes with addTrackedPostProperties

If your plugin uses includePostAttributes to add properties to the post model, you’ll need to update it to use addTrackedPostProperties instead.

Before:

api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');

After:

api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');

The addTrackedPostProperties function marks properties as tracked for post updates. This is important if your plugin adds properties to a post and uses them during rendering. It ensures the UI updates automatically when these properties change.

Common Migration Patterns

Using Plugin Outlets

Plugin outlets are your main tool for adding content at specific points in the post stream. Think of them as designated spots where you can inject your custom content.

They’re the key to migrating away from widget decorations.

The Glimmer Post Stream wraps often customized content in outlets. Use the plugin API functions renderBeforeWrapperOutlet and renderAfterWrapperOutlet to insert content before or after them.

1. Replacing widget decorations with Plugin Outlets

The most common customization is adding content to posts. In the widget system, you’d use decorateWidget. With Glimmer, you’ll use plugin outlets instead.

Before:

// Part of an initializer in a plugin

import { withPluginApi } from "discourse/lib/plugin-api";
// ... other imports

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) => {
      // ... other customizations
      customizeWidgetPost(api);
    });
  }
};

After:

// Part of a .gjs initializer in a plugin

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... other imports

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) => {
      // ... other customizations
      customizePost(api);
    });
  }
};

2. Adding content after the Poster Name

If your plugin adds content after the poster name, you’ll use the renderAfterWrapperOutlet API with the post-meta-data-poster-name outlet.

Before:

// Part of an initializer in a plugin

import { withPluginApi } from "discourse/lib/plugin-api";
// ... other imports

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) => {
      // ... other customizations
      customizeWidgetPost(api);
    });
  }
};

After:

// Part of a .gjs initializer in a plugin

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... other imports

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) => {
      // ... other customizations
      customizePost(api);
    });
  }
};

3. Adding Content Before Post Content

// Part of a .gjs initializer in a theme

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... other imports

function customizePost(api) {
  api.renderBeforeWrapperOutlet(
    "post-article",
    class extends Component {
      static shouldRender(args) {
        return args.post?.topic?.pinned;
      }

      <template>
        <div class="pinned-post-notice">
          This is a pinned topic
        </div>
      </template>
    }
  );
}

export default {
  name: "pinned-topic-notice",
  initialize() {
    withPluginApi((api) => {
      // ... other customizations
      customizePost(api);
    });
  }
};

4. Adding Content After Post Content

// Part of a .gjs initializer in a theme

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... other imports

function customizePost(api) {
  api.renderAfterWrapperOutlet(
    "post-article",
    class extends Component {
      static shouldRender(args) {
        return args.post?.wiki;
      }

      // In a real component, you would use a template like this:
      <template>
        <div class="wiki-post-notice">
          This post is a wiki
        </div>
      </template>
    }
  );
}

export default {
  name: "wiki-post-notice",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... other customizations
    });
  }
};

Using Transformers

Transformers are a powerful way to customize Discourse components. They let you modify data or component behavior without having to override entire components.

Here are some of the most relevant value transformers for post stream customization:

Transformer Name Description Context
post-class Customize CSS classes applied to the main post element. { post }
post-meta-data-infos Customize the list of metadata components displayed for a post. This allows adding, removing, or reordering items like the post date, edit indicator, etc. { post, metaDataInfoKeys }
post-meta-data-poster-name-suppress-similar-name Decide whether to suppress displaying the user’s full name when it is similar to their username. Return true to suppress. { post, name }
post-notice-component Customize or replace the component used to render a post notice. { post, type }
post-show-topic-map Control the visibility of the topic map component on the first post. { post, isPM, isRegular, showWithoutReplies }
post-small-action-class Add custom CSS classes to a small action post. { post, actionCode }
post-small-action-custom-component Replace a standard small action post with a custom Glimmer component. { post, actionCode }
post-small-action-icon Customize the icon used for a small action post. { post, actionCode }
poster-name-class Add custom CSS classes to the poster’s name container. { user }

1. Adding a Custom Class to Posts

// Part of an initializer in a plugin
import { withPluginApi } from "discourse/lib/plugin-api";

function customizePostClasses(api) {
  api.registerValueTransformer(
    "post-class",
    ({ value, context }) => {
      const { post } = context;

      // Add a custom class to posts from a specific user
      if (post.user_id === 1) {
        return [...value, "special-user-post"];
      }

      return value;
    }
  );
}

export default {
  name: "custom-post-classes",
  initialize() {
    withPluginApi((api) => {
      // ... other customizations
      customizePostClasses(api);
    });
  }
};

2. Adding Custom Post Metadata

The post-meta-data-infos transformer allows you to add custom components to the post metadata section.

// Part of an initializer in a plugin
import { withPluginApi } from "discourse/lib/plugin-api";

function customizePostMetadata(api) {
  // Define a component to be used in the metadata section
  // The component should be created outside the transformer callback, 
  // otherwise it can cause memory issues.
  const CustomMetadataComponent = <template>...</template>;

  api.registerValueTransformer(
    "post-meta-data-infos",
    ({ value: metadata, context: { post, metaDataInfoKeys } }) => {
      // Only add the component for specific posts
      if (post.some_custom_property) {
        metadata.add(
          "custom-metadata-key",
          CustomMetadataComponent,
          {
            // Position it before the date
            before: metaDataInfoKeys.DATE,
            // and after the reply tab
            after: metaDataInfoKeys.REPLY_TO_TAB,
          }
        );
      }
    }
  );
}

export default {
  name: "custom-post-metadata",
  initialize() {
    withPluginApi((api) => {
      // ... other customizations
      customizePostMetadata(api);
    });
  }
};

Here’s a real-world example from the discourse-activity-pub plugin:

// Part of an initializer in the discourse-activity-pub plugin
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);
      // ... other customizations
    });
  }
};

5. Inserting content before or after the cooked content of the post

If your plugin adds content after the text in the post, you’ll use the renderAfterWrapperOutlet API with the post-content-cooked-html outlet.

Before:

// Part of an initializer in a 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 = "This post is a wiki";
      element.prepend(banner);
    }
  });
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      // ... other customizations
      customizeCooked(api);
    });
  }
};

After:

// Part of an initializer in a plugin (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";

// Define a component to be used in the plugin outlet
class WikiBanner extends Component {
  static shouldRender(args) {
    return args.post.wiki;
  }

  <template>
    <div class="wiki-footer">This post is a wiki</div>
  </template>
}

function customizePost(api) {
  // Use renderBeforeWrapperOutlet to add content before the post content
  api.renderAfterWrapperOutlet(
    "post-content-cooked-html",
    WikiBanner
  );
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... other customizations
    });
  }
};

Supporting Both Old and New Systems During Transition

As a plugin or theme author, you may want to support both systems during the transition to ensure your extensions work with both the old and new post streams.

Here’s a pattern used by many official plugins:

// 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) {
  // glimmer post stream customizations
  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>
    }
  );

  // ...

  // wrap the old widget code silencing the deprecation warnings
  withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
    customizeWidgetPost(api)
  );
}

// old widget code
function customizeWidgetPost(api) {
  api.decorateWidget("post-contents:after-cooked", (helper) => {
    let post = helper.getModel();

    if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
      // Use RenderGlimmer to render a Glimmer component in the widget system
      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);
        // ... other customizations
      });
    }
  },
};

Real-World Examples

Here are links to actual migration pull requests from our official plugins. These show how we updated each type of customization:

Troubleshooting

My site looks broken after enabling the new post stream

  • Set glimmer_post_stream_mode back to disabled
  • Check the console for specific error messages
  • Update the problematic plugins/themes before trying again

I don’t see any warnings, but my customizations aren’t working

  • Check that your customizations target the widgets listed above
  • Make sure you’re testing on the topic page with topics where your customizations are visible

Need Help?

If you found a bug or your customization can’t be achieved using the new APIs we’ve introduced, please let us know below.

8 « J'aime »

Le flux Glimmer Post est activé sur Meta.

Si vous rencontrez des problèmes, veuillez les publier ci-dessous.

2 « J'aime »

Lors de la mise à niveau de mes composants, j’ai rencontré du code étrange :

Premièrement, je suis quelque peu perplexe quant à @user={{@post}}. Cela pourrait-il être une erreur typographique ?

Deuxièmement, pourquoi le PluginOutlet nommé post-avatar-flair et le UserAvatarFlair sont-ils des éléments distincts ? De plus, pourquoi post-avatar-flair n’est-il pas un wrapper comme les autres outlets voisins ?

Ça fonctionne, donc je ne pense pas que ce soit une faute de frappe. Peut-être qu’à cet endroit, l’objet post possède tous les attributs que le composant UserAvatarFlair s’attend à trouver sur l’argument @user ? Je suis d’accord que ça a l’air bizarre !

D’après la description de la PR, il semble que ces choses aient été faites pour la cohérence avec d’autres ‘avatar-flair’ similaires (qui ont probablement été introduits avant que les ‘wrapper plugin outlets’ n’existent).

2 « J'aime »

Nous venons de fusionner une pull request qui active le flux de publication Glimmer par défaut.

Après la prochaine mise à jour, les sites compatibles utiliseront automatiquement le nouveau flux de publication. Les sites utilisant des extensions incompatibles reviendront à l’ancienne version et afficheront des avertissements dans la console du navigateur.

Pour l’instant, vous pouvez forcer manuellement l’utilisation de l’ancien flux de publication en définissant glimmer_post_stream_mode sur disabled.

Si vous rencontrez des problèmes, veuillez les signaler ci-dessous.

5 « J'aime »

11 messages ont été divisées dans un nouveau sujet : Erreur Discourse-events ‘this.router’

Je pense que j’obtiens un avertissement à cause de ce code qui mène ensuite à ce fil de discussion :

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

Comment mettrais-je cela à jour pour le nouveau système ?

Quelque chose comme ça :

api.registerValueTransformer("post-avatar-size", () => {
    return "120";
});
2 « J'aime »

Bien, merci.

Je pense que ça fonctionne. J’ai mis ce code, puis j’ai essayé de régler temporairement tous les paramètres de force pour le mode scintillement et il semblait que la taille de l’avatar et la liste des publications fonctionnaient bien sur mon site après cela. Après les tests, j’ai désactivé les remplacements car il semble qu’ils ne soient pas encore recommandés. Mais mon site devrait maintenant être prêt pour le passage.

1 « J'aime »

Salut @Boost. :smiley:

Qu’entends-tu par le fait que les remplacements ne sont pas encore recommandés ? Si ton site est prêt, il basculera automatiquement vers le flux d’articles Glimmer.

Lors de votre prochaine mise à jour de votre installation Discourse, le Glimmer Post Stream sera désormais activé par défaut, même pour les sites qui ont encore des personnalisations incompatibles.

En fait, tous les systèmes de rendu de widgets sont maintenant désactivés, donc toute personnalisation basée sur des widgets ne sera plus rendue.

Pour l’instant, sur les sites incompatibles, l’administrateur peut réactiver l’ancien comportement en modifiant les valeurs des paramètres suivants :

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

Pour réactiver le flux de messages de widgets, les deux paramètres doivent être modifiés.

C’est la phase finale avant la suppression de l’ancien code de la base de code de Discourse, ce qui est prévu dans environ un mois. Par la suite, il ne sera plus possible de réactiver les widgets.

5 « J'aime »

L’option dans les paramètres expérimentaux met clairement en garde contre son utilisation :

Activez la nouvelle implémentation du flux de publication ‘glimmer’ en mode ‘auto’ pour les groupes d’utilisateurs spécifiés. Cette implémentation est en cours de développement actif et n’est pas destinée à une utilisation en production. Ne développez pas de thèmes/plugins contre elle tant que l’implémentation ne sera pas finalisée et annoncée.

Ainsi, la formulation est assez forte en disant qu’elle “n’est pas destinée à une utilisation en production. Ne développez pas de thèmes/plugins contre elle”.

C’est l’explication de l’option Glimmer post stream mode auto groups.

Bien vu. Nous avions manqué cela.

Le flux Glimmer Post est maintenant le flux par défaut, et la version widget est considérée comme obsolète et déjà programmée pour être supprimée. Je vais mettre à jour la description du paramètre.

3 « J'aime »