帖子流即将变更 - 如何准备主题和插件

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 个赞

Glimmer Post Stream 已在 Meta 上启用。

如果您发现任何问题,请在下方发布。

2 个赞

在升级我的组件时,我遇到了一些奇怪的代码:

首先,我对 @user={{@post}} 感到有些困惑。这会不会是印刷错误?

其次,为什么 PluginOutlet 被命名为 post-avatar-flairUserAvatarFlair 是单独的元素?另外,为什么 post-avatar-flair 不像其他附近的 outlet 那样是包装器?

它能正常工作,所以我不认为这是笔误。也许在这个位置,post 对象拥有 UserAvatarFlair 组件期望在 @user 参数上找到的所有属性?我同意它看起来很奇怪!

根据 PR 描述,看起来这些是为了与其他类似的“头像-装饰” outlet(可能是在“包装器插件 outlet”出现之前引入的)保持一致性而做的。

2 个赞

我们刚刚合并了一个默认启用 Glimmer Post Stream 的拉取请求。

下次更新后,兼容的网站将自动使用新的 Post Stream。使用不兼容扩展的网站将回退到旧版本,并在浏览器控制台中显示警告。

目前,您可以通过将 glimmer_post_stream_mode 设置为 disabled 来手动强制使用旧的 Post Stream。

如果您遇到任何问题,请在下方报告。

5 个赞

11 个帖子已拆分为新主题:Discourse-events ‘this.router’ 错误

我认为我收到了一条警告,原因是我使用了这段代码,然后它链接到了这个帖子:

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

我该如何为新系统更新它?

类似这样:

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

好的,谢谢。

我认为这可行。我输入了那段代码,然后尝试暂时开启闪光模式的所有强制设置,看起来之后我的网站上的头像大小和帖子列表都能正常工作了。测试后我关闭了覆盖设置,因为它们似乎还不推荐使用。但我的网站现在应该可以准备好切换了。

1 个赞

您好 @Boost:smiley:

您说覆盖设置还不推荐使用,这是什么意思?如果您的网站已准备就绪,它将自动切换到 Glimmer Post Stream。

下次更新 Discourse 安装时,即使对于仍有不兼容自定义的站点,Glimmer Post Stream 也将默认启用。

事实上,所有小部件渲染系统现已禁用,因此任何基于小部件的自定义将不再被渲染。

目前,在不兼容的站点上,管理员可以通过修改以下设置的值来重新启用旧行为:

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

要再次启用小部件帖子流,需要更改这两个设置。

这是在 Discourse 代码库中删除旧代码之前的最后阶段,预计将在大约一个月后进行。之后,将无法再重新启用小部件。

5 个赞

实验设置中的选项明确警告不要使用它:

在“自动”模式下为指定用户组启用新的“glimmer”帖子流实现。此实现正在积极开发中,无意用于生产环境。在实现最终确定并宣布之前,请勿基于此开发主题/插件。

因此,该措辞非常强烈地表明“无意用于生产环境。请勿基于此开发主题/插件”。

这就是 Glimmer post stream mode auto groups 选项的解释。

抓得好。我们忽略了这一点。

Glimmer Post Stream 现在是默认设置,而小部件版本被视为旧版,并已安排移除。我将更新设置说明。

3 个赞