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:
Core implementation finished
Start upgrading the official plugins and theme components
Enabled on Meta
Published upgrade advice (this guide)
Q3 2025:
- Finish upgrading the official plugins and theme components
- Set
glimmer_post_stream_mode
default toauto
and enable console deprecation messages (planned for July 2025) - Enable deprecation messages with an admin warning banner (planned for July 2025)
- Third-party plugins and themes should be updated
Q4 2025:
- Enable the new post stream by default
- 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
orattachWidgetAction
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
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
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:
- Plugin Outlets: For adding content at specific points in the post stream.
- 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 (using RenderGlimmer):
// 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:
- discourse-ai
- discourse-assign
- discourse-cakeday
- discourse-post-voting
- discourse-reactions
- discourse-shared-edits
- discourse-solved
- discourse-topic-voting
- discourse-user-notes
- discourse-activity-pub
Troubleshooting
My site looks broken after enabling the new post stream
- Set
glimmer_post_stream_mode
back todisabled
- 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.