Предстоящие изменения в потоке постов: как подготовить темы и плагины

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
  • :white_check_mark: 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.

Поток постов Glimmer включен в Meta.

Если вы обнаружите какие-либо проблемы, пожалуйста, сообщите о них ниже.

При обновлении моих компонентов я столкнулся со странным кодом:

Во-первых, я несколько озадачен конструкцией @user={{@post}}. Не является ли это опечаткой?

Во-вторых, почему PluginOutlet с именем post-avatar-flair и компонент UserAvatarFlair представляют собой отдельные элементы? Кроме того, почему post-avatar-flair не является обёрткой, как другие близлежащие outlets?

Это работает, поэтому я не думаю, что это опечатка. Возможно, в этом месте объект поста содержит все атрибуты, которые компонент UserAvatarFlair ожидает найти в аргументе @user? Согласен, выглядит это странно!

Согласно описанию PR, похоже, что эти решения были приняты ради согласованности с другими аналогичными «avatar-flair» outlets (которые, вероятно, были введены до появления «wrapper plugin outlets»).

Мы только что объединили запрос на слияние, который включает Glimmer Post Stream по умолчанию.

После следующего обновления совместимые сайты автоматически будут использовать новый Post Stream. Сайты, использующие несовместимые расширения, вернутся к старой версии и будут отображать предупреждения в консоли браузера.

На данный момент вы можете вручную принудительно включить использование старого Post Stream, установив glimmer_post_stream_mode в значение disabled.

Если вы столкнётесь с какими-либо проблемами, пожалуйста, сообщите о них ниже.

Я думаю, что получаю предупреждение из-за этого кода, который затем ссылается на эту тему:

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

Как мне обновить это для новой системы?

Что-то вроде этого:

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

Отлично, спасибо.

Кажется, это работает. Я вставил этот код, а затем временно включил все настройки силы для режима мерцания, и после этого размер аватара и список постов на моём сайте выглядели нормально. После тестирования я отключил переопределения, так как, похоже, их пока не рекомендуется использовать. Но теперь мой сайт, надеюсь, готов к переходу.

Привет, @Boost. :smiley:

Что вы имеете в виду, когда говорите, что переопределения пока не рекомендуется использовать? Если ваш сайт готов, он автоматически переключится на Glimmer Post Stream.

При следующем обновлении вашей установки Discourse Glimmer Post Stream будет включен по умолчанию даже для сайтов, у которых все еще есть несовместимые кастомизации.

Фактически, вся система рендеринга виджетов будет отключена, поэтому любые кастомизации на основе виджетов больше не будут отображаться.

На данный момент на несовместимых сайтах администратор может вернуть старое поведение, изменив значения следующих настроек:

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

Чтобы снова включить поток постов с виджетами, необходимо изменить обе настройки.

Это финальный этап перед удалением старого кода из базы кода Discourse, что ожидается примерно через месяц. После этого больше не будет возможности повторно включить виджеты.

Параметр в разделе экспериментальных настроек довольно явно предупреждает об использовании его:

Включить новую реализацию потока постов «glimmer» в режиме «auto» для указанных групп пользователей. Эта реализация находится в активной разработке и не предназначена для использования в продакшене. Не разрабатывайте темы/плагины под неё до тех пор, пока реализация не будет завершена и не будет объявлено об этом.

Таким образом, формулировка довольно категорична: «не предназначена для использования в продакшене. Не разрабатывайте темы/плагины под неё».

Это объяснение для параметра Glimmer post stream mode auto groups.

Хорошо подмечено. Мы упустили это.

Поток публикаций Glimmer теперь используется по умолчанию, а виджетная версия считается устаревшей и уже запланирована к удалению. Я обновлю описание настройки.

PR, удаляющий поток постов виджета, был принят.