(deprecated) Overriding Discourse templates from a Theme or Plugin

Ideally, when customizing Discourse via themes/plugins, you should use CSS, the JavaScript Plugin API, or plugin outlets. If none of these work for your use-case, feel free to open a PR to Discourse core or start a dev topic here on Meta. We’re always happy to discuss adding new outlets/APIs to make customization easier.

If you’ve exhausted all other options, you may need to resort to template overrides. This technique allows you to override the entire template of any Ember Component or Route from your theme/plugin.

:rotating_light: This is not a recommended way of customizing Discourse. Day-to-day changes in Discourse core will conflict with your template override eventually, potentially causing catastrophic errors when rendering the forum.

If you decide to take this approach, make sure you have sufficient automated testing and QA processes to detect regressions. If you distribute a theme/plugin with template overrides, please ensure forum admins are aware of the stability risks your theme/plugin carries.

:rotating_light: :rotating_light: :rotating_light: October 2023 Update: For new features, Discourse is increasingly moving towards using components authored using Ember’s .gjs file format. Templates for these components are defined inline, and cannot be overridden by themes/plugins.

Going forward, all template customizations should be done using Plugin Outlets

I understand this will break in the near future, show me the docs anyway

Overriding Component Templates

To override an Ember Component template (i.e. anything under components/* in Discourse core), you should create an identically-named .hbs in your theme/plugin. For example, to override the template for the badge-button component in Discourse core, you would create a template file in your theme/plugin at this location:

:art: {theme}/javascripts/discourse/templates/components/badge-button.hbs

:electric_plug: {plugin}/assets/javascripts/discourse/templates/components/badge-button.hbs

The override must always be nested inside the /templates directory, even if the core component has a ‘colocated’ template.

Overriding Route Templates

Overriding route templates (i.e. all the non-component templates under templates/*) works in the same way as components. Create an identically named template in your theme/plugin. For example, to override discovery.hbs in core, you would create a file like

:art: {theme}/javascripts/discourse/templates/discovery.hbs

:electric_plug: {plugin}/assets/javascripts/discourse/templates/discovery.hbs

Overriding ‘Raw’ Templates (.hbr)

Discourse’s “raw” template system will soon be replaced by regular Ember components. But in the meantime, overriding raw templates works in the same way as Ember templates. For example, to override topic-list-item.hbr in core, you could create a file like:

:art: {theme}/javascripts/discourse/templates/list/topic-list-item.hbr

:electric_plug: {plugin}/assets/javascripts/discourse/templates/list/topic-list-item.hbr

Interaction between multiple themes / plugins

If multiple installed themes/plugins override the same template, the ‘winner’ is the one with the lowest-numbered ranking in this list:

  1. Theme overrides (highest theme ‘id’ wins)
  2. Plugin overrides (latest alphabetical plugin name wins)
  3. Core

This precedence also means that you can override plugin templates from themes. Technically you can also override theme templates from other themes, and plugin templates from other plugins, but the behaviour can be surprising because of the dependence on plugin-name and theme-id.

How does this work?

Discourse assembles and prioritises templates in the DiscourseTemplateMap class. For colocated component templates, that information is used during app initialization to replace the core template associations. For all other templates, the map is used by the resolver at runtime to fetch the correct template.


And what about mobile templates? What is the directory structure to rewrite templates from core

It should work exactly the same - you match the name of the core template. So if it has /mobile, include that in your override.

I try to rewrite mobile login.hbs template and it doesn’t work https://i.imgur.com/zOyJ5ET.png, am I right with the path?

The full path isn’t visible in your screenshot as far as I can see. Please can you paste it here as text.


You’re missing discourse/templates from your path

So in your case, it would be {theme}/javascripts/discourse/templates/mobile/modal/login.hbs


Is this still the case?

I’m a bit sad the ability to override a lot of code is being removed.

It makes sense to replace the bespoke Widget system, to some extent, but that gave us the ability to hook into existing code at multiple levels, reducing a lot of breaking change risk as we could target just small blocks in clever ways that would allow us to:

  • add features
  • not disturb anything else.

I’ve just had to remove TWO significant features from Discourse Journal, for example, that were based on fine grain overrides to widgets because the only way to have recreated them in Glimmer is via a pair of Template overrides (including an attempt to change a .gjs file) which is apparently no longer supported.

Even if this was supported, we would be left with overriding bigger stretches of code than under the widget framework, with an associated increase in risk of core changes conflicting with the overrides.

This isn’t healthy for the extensibility of the platform.

Can anything be done about it?


Yeah I hear you - there were some nice things about widget extensibility APIs.

But the flip side is that it’s been incredibly difficult for us to modify ANY of the widget-based UI in core, because we have no idea what random methods/decorations people might be introducing. That’s why widget customisations have seemed relatively stable - we’ve been too scared to touch the core implementations.

Our solution for this going forward is Wrapper Plugin Outlets. These allow themes and plugins to optionally override very small chunks of templates with their own implementation.

For example, see how Chat conditionally overrides the home-logo with a custom component. That works for the existing widget-based header, and the new glimmer-based header (coming soon! :tm:)

We’re generally happy to accept PRs to add new wrapper outlets in various places. If you’re unsure about a particular use-case, please feel free to open a dev topic with details!


OK that’s sounds like a way forward, thank you.

I’ll need to digest the implications of that and adjust to a strategy along those lines.

Appreciate the response!