Developer’s guide to Discourse Themes

themes

(Joe) #1

So, you want to create Discourse themes? Not sure where to start? Or maybe you have created Discourse themes before, but want to learn how to do even more cool things. Well, you’ve come to the right place :wink:

Developer’s guide to Discourse Themes

Subjects include a general overview of Discourse themes, creating and sharing Discourse themes, theme development examples, searching for and finding information / examples in the Discourse repository, and best practices.

Prerequisites: Beginner's guide to using Discourse themes

:warning: While there’s very little fluff in this guide, it’s still long. It is not meant to be read in one go. To get the most out of this guide, I suggest taking your time. Go slow, and follow the examples.


Table of contents

  1. Introduction
    a. The structure of this topic
    b. The scope of this topic
  2. Overview of Discourse themes
    a. What are themes anyway?
    b. Remote and local themes
    c. Theme files and folders
    d. Color schemes
  3. Your first themes!
    a. Hello World! (HTML / CSS)
    b. Hello World! (JS)
    c. Hello World! (Remote)
    d. Creating previews on Theme Creator
  4. Advanced Discourse themes
    a. Let’s talk about CSS
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎- SCSS variables
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - A better way to find selectors
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - Reuse Discourse SCSS
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - Resue Discourse classes
    b. Handlebars templates
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - Modifying Discourse templates
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - Overriding Discourse templates
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - Mounting widgets
    c. The pluginAPI
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - console.log is your friend!
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - getCurrentUser()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - replaceIcon()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - modifyClass()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - reopenWidget()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - decorateWidget()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - createWidget()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - changeWidgetSetting()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - addNavigationBarItem()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - addUserMenuGlyph()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - onPageChange()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - decorateCooked()
    ‏‏‎ ‏‏‎ ‏‏‎ ‏‏‎ - onToolbarCreate()
    d. Theme settings
  5. Best practices
    a. Use the Discourse Theme CLI
    b. Use Prettier
    c. How to ask for help
    d. Include a license
    e. Creating a topic for your theme on Meta

1. Introduction

1.a. The structure of this tutorial

Since this topic is going to be long and will cover a wide variety of subjects, it’s good to a take a step back and describe its structure a bit. I will be using a lot of headings. The reason for this is that say at some point in the future you want a quick refresher (something that I do a lot), you can easily navigate to the section you need to look at. The headings for different sections are listed in the table of contents above. Clicking on any of those items takes you to that section.

Further, there’s a small arrow (↥) next to each heading. Clicking the arrow brings you back to the table of contents for quick navigation.

Finally, for the purposes of this guide, and unless otherwise specified, the terms “theme” and “themes” here refer to both themes and to theme components.

1.b. The scope of this topic

Let’s start by highlighting what this topic is all about.

Introduction to Discourse theme development for developers with little or no previous experience working on Discourse themes. Developers learn how to create Discourse themes that either modify the design of a forum or add new functionality. While this topic assumes no previous experience working on Discourse themes, it does assume some experience in the languages it covers.

Those languages are listed below - each is a link to a good place to read more about the language.

  1. HTML
  2. CSS
  3. JavaScript / jQuery
  4. SCSS
  5. Handlebars

It’s also a good idea to know your way around Github

Don’t stress about! You won’t need to learn / know all of these to create a theme. I have to include everything here for this guide to be a good reference point.

The beauty of Discourse themes is that they will go as far as you take them.

Want to create a simple CSS theme component that adds hover effects to titles?
You can.

Want to create a mega-complex monolith theme that uses SCSS / Handlebars / Ajax and completely overhauls all the the things?
You can.

In order to scope this tutorial.

Think of it this way. This tutorial will not teach you how to write js conditional statements. It will, however, teach you how to find and feed Discourse specific bits into conditional statements.

Ok, now we covered the scope and structure, we can move on to the bits you’re actually interested in reading.


2. Overview of Discourse themes

2.a. What are themes anyway?

In a previous guide, I described themes / theme components as:

Those definitions come from the Beginner’s guide to using Discourse themes (probably worth glancing over if you haven’t done so already) which is geared towards users of Discourse themes and not Discourse theme developers. However, this is a good base to start with.

Beyond the definitions above, here’s what you need to know as a theme developer.

Themes can only amend the front end and have no access to the backend. If this makes little to no sense to you, you don’t need to worry about it for now.

2.b. Remote and local themes

Discourse themes and theme components can either be

  1. Local
  2. Remote

Local themes are themes created / stored on a Discourse install.

You create a new local theme by clicking here:

After making your changes. These can be exported and saved / shared by clicking here:

but sharing a file somewhere is not the ideal way to share a theme publicly, so we have remote themes.

Remote themes are Discourse themes that live in repositories on Github. This makes it easy to share themes. You create a theme and share the link to the repository, then users can import the theme using that link by clicking here:

all the themes we have in the #theme categories are remote themes. Here’s an example of what one of them looks like on Github

2.c. Theme files and folders

Let’s look at the interface for the theme editor:

Notice how there are three main sections.

  1. Common
  2. Desktop
  3. Mobile

As you may have already guessed, this allows your theme to target different device types, or apply your changes to both. Anything you change in the common tab will apply to desktop and mobile. Anything you change in the desktop or mobile tab will only apply to that respective device type.

Now let’s look at the subsections under those.

  1. CSS
  2. <head>
  3. Header
  4. After Header
  5. </body>
  6. Footer
  7. Embedded CSS

First a little bit about those subsections:

  1. CSS: You can add CSS and SCSS here. Whatever you add is complied automatically on save and added as a separate .css stylesheet to the <head> section if the theme is active.

  2. <head>: You can add html here (including script tags). Anything you add here is inserted just before the close tag of the <head> close tag or </head>

  3. Header: You can also add html here. Anything you add here is inserted at the top of the <body> tag before the Discourse Header

  4. After Header: Same as above. You can add html here. However, anything you add here will be inserted below the Discourse header but above the rest of the page content

  5. </body>: You can use html here. Anything you add is inserted at the very bottom of the <body> tag or just before the </body> tag

  6. Footer: You can also use html here but it’s is inserted just after the end of the page content or just below the <div id="main-outlet"> close tag

  7. Embedded CSS: You can add CSS and SCSS here as well, the difference is that whatever you add here is only applied to Discourse when it’s embeded on another site.

This covers all the subsections of the common section of the theme editor. The desktop and mobile sections are exactly the same except that they will only target their respective devices and they don’t have the Embedded CSS subsection.

Now that you understand what those sections are. Here are (most of) the files that a theme can contain:

common/common.scss
common/head_tag.html
common/header.html
common/after_header.html
common/body_tag.html
common/footer.html
common/embedded.scss

desktop/desktop.scss
desktop/head_tag.html
desktop/header.html
desktop/after_header.html
desktop/body_tag.html
desktop/footer.html

mobile/mobile.scss
mobile/head_tag.html
mobile/header.html
mobile/after_header.html
mobile/body_tag.html
mobile/footer.html

We’re pretty serious about making sure file names are intuitive and so I hope that you can glance at the file name and figure out which section it relates to.

I said most of the files above because there are also other files a theme can / must include.

A theme can include assets like fonts and images and those go into an assets folder.

assets/background.jpg
assets/font.woff2
assets/icon.svg

A remote theme must include an about.json file in order for it to importable. The about.json file lives at the root of the theme and its contents look like this

{
  "name": "Theme name",
  "about_url": "Theme about url",
  "license_url": "Theme license url",
   "assets": {
        "asset-variable": "assets/background.svg"
   },
   "color_schemes": {
       "color scheme name": {
          "primary": "000000",
          "secondary": "000000",
          "tertiary": "000000",
          "quaternary": "000000",
          "header_background": "000000",
          "header_primary": "000000",
          "highlight": "000000",
          "danger": "000000",
          "success": "000000",
          "love": "000000"
        }
    }
}

Themes can also include settings. The settings live in a settings.yml file that also live at the root of the theme directory. A theme settings file looks like this:

whitelisted_fruits:
  default: apples|oranges
  type: list

favorite_fruit:
  default: orange
  type: enum
  choices:
    - apple
    - banana

This is just an example of theme settings. More details about them will follow.

So, here’s an example of what a finished theme would look like:

about.json

assets/font.woff2
assets/background.jpg
assets/icon.svg

common/common.scss
common/head_tag.html
common/header.html
common/after_header.html
common/body_tag.html
common/footer.html
common/embedded.scss

desktop/desktop.scss
desktop/head_tag.html
desktop/header.html
desktop/after_header.html
desktop/body_tag.html
desktop/footer.html

mobile/mobile.scss
mobile/head_tag.html
mobile/header.html
mobile/after_header.html
mobile/body_tag.html
mobile/footer.html

settings.yml

pretty straightforward huh?

No! The only thing required is the about.json file for remote themes. Everything else is optional and should only be added if you need it.

2.d. Color schemes

You probably noticed the color_schemes section in the about.json file example above. Well, themes can introduce new color schemes!

A Color scheme is a set of colors you choose that is used to automatically color all the elements in Discourse.

The interface looks like this

and the colors are linked to the values you enter in the about.json file we discussed earlier. So

"color_schemes": {
  "Foo bar": {
      "primary": "cccccc",
      "secondary": "111111",
      "tertiary": "009dd8",
      "quaternary": "9E9E9E",
      "header_background": "131418",
      "header_primary": "cccccc",
      "highlight": "9E9E9E",
      "danger": "96000e",
      "success": "1ca551",
      "love": "f50057"
    }
}

would create a new color scheme that looks like this

Discourse then takes those colors (if that color scheme is active) does a bit of magic to them (which we’ll cover later) and creates a few variations of those colors to style all the elements. This removes the need to write a gazillion lines of CSS just to change the theme colors across the board.

Fine. :sob:

Your first themes!

3.a. Hello World (HTML / CSS)

Let’s start with a basic local theme. We’re going to add a big “Hello World!” banner under the Discourse header. Like I mentioned at the intro, this topic will not teach you how to write CSS / HTML so I won’t be explaining those but here are the bits we’re going to need for this theme.

html

<div class="hello-world-banner">
   <h1 class="hello-world">Hello World!</h1>
</div>

CSS

.hello-world-banner {
    height: 300px;
    width: 100%;
    background: red;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1em;
}

.hello-world {
    font-size: 8em;
    color: white;
}

So, since we want our banner here

Then the best place to add the html for it would be the After Header section in the theme editor or here

and the CSS goes here

Be sure to hit the save button and then hit the preview button

and if we check to see…

There! You’ve just created your first Discourse theme! :tada:

3.b. Hello World (JS)

We will now try to do something similar but with JS. Let’s try to create an alert that says “Hello World!” when you first load Discourse.

The script we’re going to need is

<script>
    alert('Hello world!')
</script>

Now, since this is a script, we need be a little bit more careful where to add to ensure that it fires. You have one of three options:

  1. The </head> section
  2. The Header section
  3. The </body> section

Adding the script to any other section will cause it not to fire. I prefer to keep scripts in the </head> or Header sections. So, let’s add it to the </head> section like so:

Save, and check if it worked

Hooray! :tada: Your second basic Discourse theme is done.

3.c. Hello World (Remote)

As we discussed earlier, remote themes live in Github repositories. So, let’s go ahead and create a new repository and add a license to it

When in doubt, Select MIT for the license.

Now we have a repository! But it’s a bit…empty. So let’s fix that. We’re going to recreate the (HTML / CSS) “Hello World!” theme you created locally earlier.

If you remember, We added

<div class="hello-world-banner">
   <h1 class="hello-world">Hello World!</h1>
</div>

To the After Header subsection in the common section

So we need to do the same with your first remote theme. If you recall from earlier, I mentioned that if you need to add html to the common After Header section of a theme, you need your remote repository to contain a folder named common, with a file named after_header.html in it. So, let’s create that and add the markup for the “Hello World!” banner there.

Then commit the new file.

We also need to add the banner CSS. So, we need to create a file named common.scss in the same common folder and add the CSS to it. So this

.hello-world-banner {
    height: 300px;
    width: 100%;
    background: red;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 1em;
}

.hello-world {
    font-size: 8em;
    color: white;
}

Is added like so:

You can then commit the new common.scss file.

Now, your first remote theme does not have any settings or assets, so the only file left for you to make is the about.json file. As we discussed before. about.json files are required for remote themes to work.

This:

{
  "name": "My first remote theme",
  "about_url": "https://some.url.com",
  "license_url": "https://github.com/GitHubUsername/my-first-remote-theme/blob/master/LICENSE"
}
  1. the name of your theme
  2. The “About” URL for your theme, which shows up here for users of your theme:

  1. The url for the theme’s license, which you can get by clicking on the license file of your repository (or you can use any license URL you have like here)

The link to the theme’s license show’s up here in for users of your theme:

So, now you know what to add to the about.json file of your theme, let’s go ahead and make one. It needs to be at the root of your repository.

Commit the about.json file and your theme should be ready to be imported.

Let’s try to import your first remote theme. Copy the repository’s link from here

Then go to the theme editor and click on Import here

Then on “From the web” and paste the repository link in the input like so

Click on Import…and… Magic!

Try to preview your first remote theme to make sure everything looks right by clicking here

and…:drum:

There it is! Your first remote Discourse Theme! :tada::tada:

You can share the repository url with anyone and they will be able to install your theme with only a few clicks but we can take this a step further!

This.

3.d. Creating previews on Theme Creator

Not long ago, we introduced Theme Creator

Theme creator is a tool that allows theme developers to

  1. Create themes (without installing Discourse)
  2. Have content to test themes on (not so easy on an empty install)
  3. Create previews for themes (super easy to show your work!)

While logged in here on Meta, visit

https://theme-creator.discourse.org

hit login… done!

You now have an account on theme creator and can create and share themes.

Once logged in, you’ll see this:

Now click on My themes and you’ll be taken to a familiar interface:

The reason this interface looks familiar is that it’s the same you would see on your own Discourse install. However, with theme creator, you now have access to it even if you don’t have Discourse installed!

Now, let’s try to import your first remote theme to theme creator like we did earlier

Now, let’s create a preview link for your theme on theme creator. All you need is to click here and give it a name.

copy the link

Done! now you can share this link with anyone and they will be able to preview your theme on a live Discourse install

Here’s mine:

https://theme-creator.discourse.org/theme/Johani/my-first-remote-theme

Now, when users click that link they will see

Very cool, huh? :wink::+1:

Well, I’m glad you asked.

4. Advanced Discourse themes

4.a. Let's talk about CSS

Discourse uses SCSS to simplify styling and increase maintainability. I won’t get into the details of why it is like that. I’ll just leave it at recommending that you use SCSS instead of CSS when possible in your themes. It scales a lot better and your future self will thank you!

- SCSS variables

Because Discourse uses SCSS, you can use variables like so
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
  font: 100% $font-stack;
  color: $primary-color;
}

It’s very easy to change one line at the top of your sheet to change either the color or font when compared to changing hardcoded colors for tens of different elements.

Additionally, you can use Discourse core variables in your theme. This includes color schemes and lot of other things. Going back to the “Hello World!” banner theme from earlier, we had this CSS

.hello-world-banner {
 height: 300px;
 width: 100%;
 background: red;
 display: flex;
 align-items: center;
 justify-content: center;
 margin-bottom: 1em;
}

.hello-world {
 font-size: 8em;
 color: white;
}

Notice how the backgrond color is hardcoded to red and the font color for the heading is hardcoded to white

Let’s try to use the color scheme variables instead. These look like so

$primary
$secondary
$tertiary
$quaternary
$header_background
$header_primary
$highlight
$danger
$success
$love

and the names should sound familiar to you because they are the same as here

If we attempt to use those color instead of hardcoded values, we’d end up with this

.hello-world-banner {
  height: 300px;
  width: 100%;
  background: $quaternary;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 1em;
}

.hello-world {
  font-size: 8em;
  color: $secondary;
}

and here’s the result

now, if the active color scheme changes, your theme will adjust itself magically!

This is just an example of one of the variable types you can use in your theme. We have a lot more and there’s a topic detailing those here:

- A better way of finding CSS selectors

The amount of elements in Discourse can look a bit overwhelming from a re-styling stand point. However, this feeling is rooted in trying to use traditional approaches on a very modern web app like Discourse. You can honestly write 4-8k lines of pure “traditional” CSS and you’d barely be close to a full theme.

This is part of the reason why I recommend you use SCSS.

let’s assume you want to style all the buttons in Discoruse. Well, you can use DevTools and try to find every variation of every button and style it, or you can try a different approach. The approach is to reuse whatever you can.

- Reuse Discourse SCSS

Let’s try to restyle all the buttons in Discourse by using this approach

  1. Open DevTools
  2. Highlight a button
  3. Find the origin stylesheet for its styles
  4. Copy the selectors

With screenshots

There, now you have organised SCSS selectors that match those used on Discourse core.

Expand snippet
// --------------------------------------------------
// Buttons
// --------------------------------------------------

// Base
// --------------------------------------------------

.btn {
  display: inline-block;
  margin: 0;
  padding: 6px 12px;
  font-weight: 500;
  font-size: $font-0;
  line-height: $line-height-medium;
  text-align: center;
  cursor: pointer;
  transition: all 0.25s;

  &:active,
  &.btn-active {
    text-shadow: none;
  }
  &[disabled],
  &.disabled {
    cursor: default;
    opacity: 0.4;
  }
  .fa {
    margin-right: 7px;
  }
  &.no-text {
    .fa {
      margin-right: 0;
    }
  }
}

.btn.hidden {
  display: none;
}

// Default button
// --------------------------------------------------

.btn {
  border: none;
  color: $primary;
  font-weight: normal;
  background: $primary-low;

  &[href] {
    color: $primary;
  }
  &:hover,
  &.btn-hover {
    background: $primary-medium;
    color: $secondary;
  }
  &[disabled],
  &.disabled {
    background: $primary-low;
    &:hover {
      color: dark-light-choose($primary-low-mid, $secondary-high);
    }
    cursor: not-allowed;
  }

  .d-icon {
    opacity: 0.7;
    line-height: $line-height-medium; // Match button text line-height
  }
  &.btn-primary .d-icon {
    opacity: 1;
  }
}

// Primary button
// --------------------------------------------------

.btn-primary {
  border: none;
  font-weight: normal;
  color: $secondary;
  background: $tertiary;

  &[href] {
    color: $secondary;
  }
  &:hover,
  &.btn-hover {
    color: #fff;
    background: dark-light-choose($tertiary, $tertiary);
  }
  &:active,
  &.btn-active {
    @include linear-gradient($tertiary, $tertiary);
    color: $secondary;
  }
  &[disabled],
  &.disabled {
    background: $tertiary;
  }
}

// Danger button
// --------------------------------------------------

.btn-danger {
  color: $secondary;
  font-weight: normal;
  background: $danger;
  &[href] {
    color: $secondary;
  }
  &:hover,
  &.btn-hover {
    background: scale-color($danger, $lightness: -20%);
  }
  &:active,
  &.btn-active {
    @include linear-gradient(scale-color($danger, $lightness: -20%), $danger);
  }
  &[disabled],
  &.disabled {
    background: $danger;
  }
}

// Social buttons
// --------------------------------------------------

.btn-social {
  color: #fff;
  &:hover {
    color: #fff;
  }
  &[href] {
    color: $secondary;
  }
  &:before {
    margin-right: 9px;
    font-family: FontAwesome;
    font-size: $font-0;
  }
  &.google,
  &.google_oauth2 {
    background: $google;
    &:before {
      content: $fa-var-google;
    }
  }
  &.instagram {
    background: $instagram;
    &:before {
      content: $fa-var-instagram;
    }
  }
  &.facebook {
    background: $facebook;
    &:before {
      content: $fa-var-facebook;
    }
  }
  &.cas {
    background: $cas;
  }
  &.twitter {
    background: $twitter;
    &:before {
      content: $fa-var-twitter;
    }
  }
  &.yahoo {
    background: $yahoo;
    &:before {
      content: $fa-var-yahoo;
    }
  }
  &.github {
    background: $github;
    &:before {
      content: $fa-var-github;
    }
  }
}

// Button Sizes
// --------------------------------------------------

// Small

.btn-small {
  padding: 3px 6px;
  font-size: $font-down-1;
}

// Large

.btn-large {
  padding: 9px 18px;
  font-size: $font-up-1;
  line-height: $line-height-small;
}

.btn-flat {
  background: transparent;
  border: 0;
  outline: 0;
  line-height: $line-height-small;
  .d-icon {
    opacity: 0.7;
  }
}

Once you make the changes to fit your needs and save, you will already have created a theme component that changes the way all buttons in Discourse appear.

Obviously, you’d ideally only save the selectors you intend on modifying and remove unchanged rules.

Much easier than finding all the buttons / selectors one by one, no? :wink:

- Reuse Discourse classes

On a similar note, you can make your life a lot easier by using Discourse classes in your html instead of rewriting the styles. Here’s what I mean, let’s say you want to add a couple of buttons above the header. You’d start with something like

<div>
    <button>Click me!</button>
    <button>Don't click me!</button>
</div>

in the Header section of a theme. This html by itself would look like this:

Now, this doesn’t look right, it needs to be styled. There are two ways to do this, you can either write the SCSS needed to style these new elements, which can take a bit of time, or you can simply reuse Discourse core classes.

For example, if you check the #main-outlet element which wraps around the entire content, you’ll find it has the class wrap

Now if we reuse that class, along with a couple of other classes in the example html we end up with this

<div class="wrap">
    <button class="btn btn-primary">Click me!</button>
    <button class="btn btn-danger">Don't click me!</button>
</div>

and it looks a bit better, even though we haven’t added any CSS.

Once you’re sure you can add any more reusable classes from Discourse core, you can then write your custom css and classes like so

html

<div class="wrap foobar">
    <button class="btn btn-primary">Click me!</button>
    <button class="btn btn-danger">Don't click me!</button>
</div>

SCSS

@import "common/foundation/variables";

.foobar {
  display: flex;
  justify-content: flex-end;
  background: $secondary-high;
  padding: .5em 0;
  button {
    margin: .25em;
  }
}

And again, since we have not hardcoded any colors in the design, it will follow the current active color scheme.

This is where Handlebars templates come in.

4.b. Handlebars templates

Discourse is a modern web app. Traditional HTML by itself is not flexible enough due to the dynamic nature of content on Discourse. Just like SCSS makes working with CSS a lot easier, using Handlebars templates makes working with HTML less of a hassle.

If you’re already familiar with Handlebars templates, then great! If not, don’t worry about it and just think of Handlebars template as html on steroids.

- Modifying Discourse templates

The easiest way to add html to any template is to find a plugin-outlet

Well as it turns out most Discourse templates have things like this in them

{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}

This particular one comes from this template

Well, you can do something like this (I’ll explain in a bit)

<script type="text/x-handlebars" data-template-name="/connectors/topic-above-post-stream/foobar">
  <div style="height: 200px; width: 200px;background: red"></div>
</script>

Now let’s break this down a bit.

<script type="text/x-handlebars" data-template-name="/connectors/topic-above-post-stream/foobar">

Look at the data-template-name attribute above. Notice anything familiar?

Well, when you need to add raw html or Handlebars expressions to either this

<div style="height: 200px; width: 200px;background: red"></div>

or this

{{d-button class="btn-small cancel-edit" icon="times"}}

Discourse wants to know where you want to add those new elements. Plugin-outlets are just the way to do that. When I specify

data-template-name="/connectors/topic-above-post-stream/

in the Handlebars script tag, I am literally saying to Discourse, take the content of this script tag and inject it where the topic-above-post-stream plugin outlet is.

Naming is critical. so be sure to follow this

<script type="text/x-handlebars" data-template-name="/connectors/PLUGIN-OUTLET-NAME/UNIQUE-NAME">

</script>

Remember the example I gave you above?

<script type="text/x-handlebars" data-template-name="/connectors/topic-above-post-stream/foobar">
  <div style="height: 200px; width: 200px;background: red"></div>
</script>

Here’s what that results in

I’ve just added a random red box above the post stream on topic pages.

Let’s try something else

<script type="text/x-handlebars" data-template-name="/connectors/category-title-before/foobar2">
  <div style="height: 25px; width: 25px;background: red"></div>
</script>

Try to read :point_up: this and see if you can figure out what I’m trying to do there…

Well, I added a tiny box before the title of every category.

here it is

The easiest way to find plugin-outlets is to search for plugin-outlet in the Discourse repository

https://github.com/discourse/discourse/search?q=plugin-outlet

You can submit a PR to add it! As long as the plugin-outlet has valid uses, we’ll gladly add it to Discourse core.

Well, this is where overriding Handlebars templates comes in

- Overriding Discourse templates

Just like you can add new elements to templates, you can make changes to existing elements by overriding the template. A word of caution first. When you override a template, you’re essentially using a new template in its place. While this is not inherently a bad thing. it does come with increased maintenance.

If you override a template (the topic-list-item for example) and subsequent changes are made to that template in Discourse core, you need to make sure you update your template to make sure everything works as expected. Think of it like maintaining a fork on Github.

Fine… :expressionless:

Here’s how to override a template

<script type="text/x-handlebars" data-template-name="application">

</script>

It’s very similar to how you would modify a template with a plugin-outlet, the difference being the way you specify the data-template-name attribute.

When you specify a template to override, you need to know its name. In the Discourse repository, all templates live here (bookmark that page)

discourse/app/assets/javascripts/discourse/templates at master · discourse/discourse · GitHub

What you need to add as the data-template-name attribute is the template name minus the extension so

application.hbs

becomes

data-template-name="application"

While not obvious in the example above, this is actually the path of the file relative to the templates folder. application.hbs lives at the root of that folder, so nothing else needs to be added. However, if the template you want to target is inside a subfolder inside the templates folder, you need to specify that as well.

For example

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs

is inside the list subfolder in the templates folder, so to target it you need to write

data-template-name="list/topic-list-item.raw"

Also note that I kept the .raw part of the template name above. You only need to remove the .hbs part of the name. So, we’re going to take this:

<script type="text/x-handlebars" data-template-name="list/topic-list-item.raw">

</script>

Copy / paste the contents of the core template inside it first

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/templates/list/topic-list-item.raw.hbs

And then make whatever modifications we need there. For example, we can remove all poster avatars using something like this

Expand snippet
<script type="text/x-handlebars" data-template-name="list/topic-list-item.raw">
{{#if bulkSelectEnabled}}
  <td class="bulk-select">
    <input type="checkbox" class="bulk-select">
  </td>
{{/if}}

{{!--
  The `~` syntax strip spaces between the elements, making it produce
  `<a class=topic-post-badges>Some text</a><span class=topic-post-badges>`,
  with no space between them.
  This causes the topic-post-badge to be considered the same word as "text"
  at the end of the link, preventing it from line wrapping onto its own line.
--}}
<td class='main-link clearfix' colspan="{{titleColSpan}}">
  <span class='link-top-line'>
    {{~raw-plugin-outlet name="topic-list-before-status"}}
    {{~raw "topic-status" topic=topic}}
    {{~topic-link topic class="raw-link raw-topic-link"}}
    {{~#if topic.featured_link}}
      {{~topic-featured-link topic}}
    {{~/if}}
    {{~raw-plugin-outlet name="topic-list-after-title"}}
    {{~#if showTopicPostBadges}}
      {{~raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl newDotText=newDotText}}
    {{~/if}}
  </span>

  {{discourse-tags topic mode="list" tagsForUser=tagsForUser}}
  {{#if expandPinned}}
    {{raw "list/topic-excerpt" topic=topic}}
  {{/if}}
  {{raw "list/action-list" topic=topic postNumbers=topic.liked_post_numbers className="likes" icon="heart"}}
</td>

{{#unless hideCategory}}
  {{#unless topic.isPinnedUncategorized}}
    {{raw "list/category-column" category=topic.category}}
  {{/unless}}
{{/unless}}

{{#if showPosters}}
  {{raw "list/posters-column" posters=topic.posters}}
{{/if}}

{{raw "list/posts-count-column" topic=topic}}

{{#if showParticipants}}
  {{raw "list/posters-column" posters=topic.participants}}
{{/if}}

{{#if showLikes}}
<td class="num likes">
  {{#if hasLikes}}
    <a href='{{topic.summaryUrl}}'>
      {{number topic.like_count}} {{d-icon "heart"}}</td>
    </a>
  {{/if}}
{{/if}}

{{#if showOpLikes}}
<td class="num likes">
  {{#if hasOpLikes}}
    <a href='{{topic.summaryUrl}}'>
      {{number topic.op_like_count}} {{d-icon "heart"}}</td>
    </a>
  {{/if}}
{{/if}}

<td class="num views {{topic.viewsHeat}}">{{number topic.views numberKey="views_long"}}</td>

{{raw "list/activity-column" topic=topic class="num" tagName="td"}}
</script>

Notice the red X. This part of the topic list comes from another template and so you’d need to find that and remove it as well for your design to be consistent… but the change we just made removed all avatar images from the topic list.

As well as removing elements from template, you can add new ones, just like with plugin-outlets and you can also move things around are reorder the template to your liking.

So, let’s try to add a sidebar on desktops next to the latest topic list.

For this we’re going to need to override the components/topic-list template. Or this

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/templates/components/topic-list.hbs

Here’s the html for the (basic) sidebar

<div class="sidebar">
  <div class="card"></div>
  <div class="card"></div>
  <div class="card"></div>
</div>

and here’s the SCSS

@import "common/foundation/variables";

.sidebar {
  background: $secondary-high;
  padding: 0 1em;
  .card {
  background: $secondary;
  height: 200px;
  width: 200px;
  padding: .5em;
  margin: .5em 0;
  box-sizing: border-box;
  }
}

table.topic-list {
  display: flex;
}

We’re going to to add the HTML to the components/topic-list template and make other adjustments like so

Expand snippet
<script type="text/x-handlebars" data-template-name="components/topic-list">
<div class="sidebar">
  <div class="card"></div>
  <div class="card"></div>
  <div class="card"></div>
</div>
{{plugin-outlet
  name="before-topic-list-body"
  args=(hash
    topics=topics
    selected=selected
    bulkSelectEnabled=bulkSelectEnabled
    lastVisitedTopic=lastVisitedTopic
    discoveryList=discoveryList
    hideCategory=hideCategory)
  tagName=""
  connectorTagName=""}}
<tbody>
    {{raw "topic-list-header"
      canBulkSelect=canBulkSelect
      toggleInTitle=toggleInTitle
      hideCategory=hideCategory
      showPosters=showPosters
      showLikes=showLikes
      showOpLikes=showOpLikes
      showParticipants=showParticipants
      order=order
      ascending=ascending
      sortable=sortable
      listTitle=listTitle
      bulkSelectEnabled=bulkSelectEnabled}}
  {{#each filteredTopics as |topic|}}
    {{topic-list-item topic=topic
                      bulkSelectEnabled=bulkSelectEnabled
                      showTopicPostBadges=showTopicPostBadges
                      hideCategory=hideCategory
                      showPosters=showPosters
                      showParticipants=showParticipants
                      showLikes=showLikes
                      showOpLikes=showOpLikes
                      expandGloballyPinned=expandGloballyPinned
                      expandAllPinned=expandAllPinned
                      lastVisitedTopic=lastVisitedTopic
                      selected=selected
                      tagsForUser=tagsForUser}}
    {{raw "list/visited-line" lastVisitedTopic=lastVisitedTopic topic=topic}}
  {{/each}}
</tbody>
</script>

And here’s our basic sidebar :tada:

As an aside: mobile templates are inside the mobile subfolder in the templates folder. You would do the exact same thing if you want to modify any mobile template. Just be mindful of the path and the file name in the data-template-name attribute like we discussed before.

- Mounting widgets

Since modifying / overriding templates is something you might be doing quite a bit, let’s something a litte bit more advanced. We’re going to mount a widget and add it to a template.

Since we’re still in the Handlebars section of the guide, I’m not going to spend any time explaining what widgets are. I’ll do that later, but for now I can give you examples of some Discourse widgets.

  • The header is a widget
  • The header logo is widget
  • The hamburger menu is a widget
  • The categories list inside the hamburger menu is widget

These are just examples of widgets. For now, you can think of them as blocks. All Discourse widgets live here

https://github.com/discourse/discourse/tree/master/app/assets/javascripts/discourse/widgets

Widgets render faster and more faster is more better :stuck_out_tongue:

I think the next example will help. First, let’s pick a widget. I’ll pick the home-logo widget, or this

We’re going to create a footer theme component and dynamically add the site logo to it. For this I chose to go with the plugin-outlet route instead of overriding a template because there’s a plugin-outlet that works for our purposes

{{plugin-outlet name="below-footer" args=(hash showFooter=showFooter)}}

which you can find here

So, based on our previous discussion about plugin-outlets, we’re going to need something like this

<script type="text/x-handlebars" data-template-name="/connectors/below-footer/fancy-footer">

</script>

Now, mounting a widget is pretty simple, all you need to know is the widget’s name. That’s it. We already know the name of the widget we want to use and it’s home-logo and so here’s what we need in order to mount it and add it to the template via the plugin-outlet

{{mount-widget widget="home-logo"}}

Now we add a bit of HTML around it like so

<script type="text/x-handlebars" data-template-name="/connectors/below-footer/fancy-footer">
<div class="footer">
  <div class="wrap">
    <ul>
      <li><a href="/about">About</a></li>
      <li><a href="/Privacy">Privacy</a></li>
      <li><a href="/TOS">Terms of Service</a></li>
    </ul>
    <div class="footer-logo">
      {{mount-widget widget="home-logo"}}
    </div>
  </div>
</div>
</script>

and a sprinkle of SCSS

@import "common/foundation/variables";

.footer {
  background: $primary-low;
  .wrap {
    display: flex;
  }
  ul {
    display: flex;
    flex: 1;
    margin: 0;
  }
  li {
    list-style: none;
    margin: 1em;
    font-size: $font-up-1;
    color: $secondary;
  }
  .footer-logo {
    display: flex;
    align-items: center;
    img {
      max-height: 40px;
    }
  }
}

annnnnd…

:tada:

I’m glad you asked. Meet the pluginAPI :sunglasses:

4.c. The pluginAPI

In a nutshell, the PluginAPI is an easy way for you to write JS / jQuery and make Discourse do things amazing things!

Here are a couple of examples.

Brand header theme component:

This theme component uses the pluginAPI to create new widgets. Those widget include a brand header above the Discourse header and a new hamburger menu on mobile. It looks like this:

Discourse category banners:

This component uses the pluginAPI to create dynamic banners and place them at the top of each category page, it automatically fetches the category name, description and color and it looks like this:

You start here:

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/lib/plugin-api.js.es6

This is the file in the Discourse repository where all the pluginAPI methods are defined.

I will go through the one’s you’re most likely to use and provide examples as a way of explaining how they work, but first, a bit of background.

When you use the pluginAPI, you have to use special <script> tag attributes in order for things to work properly. You’re probably very used to seeing things like

<script>
    alert('Hello world!')
</script>

or

<script type="text/javascript">
    alert('Hello world!')
</script>

And what you need for the pluginAPI is this

<script type="text/discourse-plugin" version="0.8">
    alert('Hello world!')
</script>

The two new things here are the type attribute and the version attribute.

The type attribute is self explanatory. The version attribute helps you ensure stability. Say for example you create a theme that uses a pluginAPI method that was just introduced to Discourse core. You would then set the version to one that matches the core pluginAPI version number that introduced the new method.

In case someone with an outdated Discourse installs your theme. They would then see a message in the console instead of their site breaking.

Which brings me to the next point.

- console.log is your friend!

I cannot begin to emphasise the importance of using console.log() enough. If you’re ever lost, or not sure about something always use console.log()

Let’s try this

<script type="text/discourse-plugin" version="0.8">
    console.log(Discourse)
</script>

What I’ve done here is logged Discourse which is a global object to the console. Now if I check and see what I get

you’ll notice a vary large amount of things are now available to me to use, for example, site settings, which I highlighted above.

But this is just a warm up, let’s try a quick demo

<script type="text/discourse-plugin" version="0.8">
  const settings = Discourse.SiteSettings,
    taggingEnabled = settings.tagging_enabled,
    title = settings.title;

  if (taggingEnabled) {
    console.log("Yay! "+title+" has tagging enabled!")
  } else {
    console.log("Ohh nooos! "+title+"Does not allow tagging.")
  }
</script>

As it turns out, Theme creator does allow tags to be used, so if I check the console

53

So, I’ll say this one more time for good measure, if you’re lost at any point use

console.log($(this))

and check the console to see what you have to work with.

So, with all of that out of the way, here are the methods currently in the pluginAPI

- getCurrentUser()

This method allows you get information about the current user. if we try something like this

<script type="text/discourse-plugin" version="0.8">
  const user = api.getCurrentUser();
  
  console.log(user)
</script>

We can easily find things like the the current user’s username. Now let’s try to do something with that information on our previous “hello world!” banner.

<script type="text/discourse-plugin" version="0.8">
  $( document ).ready(function() {
    const user = api.getCurrentUser(),
      username = user.username;

    $('h1.hello-world').html('Hello there '+username+"!")
  });
</script>

Where username will be dynamic and will match the current user’s username.

There’s a lot more than username for you to use, but I wanted to keep it simple. Use

<script type="text/discourse-plugin" version="0.8">
  const user = api.getCurrentUser();
  
  console.log(user)
</script>

Check the console and see what else you can use.

- replaceIcon()

api.replaceIcon(source, destination)

With this method, you can easily replace any Discourse icon with another. For example, we have a theme component that replaces the heart icon for like with a thumbs-up icon

that uses something like this

api.replaceIcon('heart', 'thumbs-up');

And it looks like this

55

- modifyClass()

You can use this method to extend or overwrite methods in a class like a component or controller (read: Ember Classes), but it’s also a great way to get information and set variables.

<script type="text/discourse-plugin" version="0.8">
  api.modifyClass('controller:composer', {
    actions: {
      newActionHere() { }
    }
  });
</script>

Don’t get confused by all the new terms here.

Ember components are used to encapsulate markup and style into reusable content. Components consist of two parts: a JavaScript component file that defines behavior, and its accompanying Handlebars template that defines the markup for the component’s UI.

For the purposes of this guide, Think of controllers in the same way.

Let’s pick a controller and play around and see what we can achieve. In the Discourse repository, all controllers live here

https://github.com/discourse/discourse/tree/master/app/assets/javascripts/discourse/controllers

I’m going to pick the composer controller and we’re going to try to capture every keypress in the editor. First, let’s take a look at what’s available for us to use.

This looks very close to what I want to achieve, so we start with this

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("controller:composer", {

});
</script>

Then add the action we want to overwrite as is

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("controller:composer", {
  actions: {
    typed() {
      this.checkReplyLength();
      this.get("model").typing();
    }
  }
});
</script>

And then we finally add our change

console.log("typed a letter");

Which should leave a message in the console at ever keystroke

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("controller:composer", {
  actions: {
    typed() {
      console.log("typed a letter");
      this.checkReplyLength();
      this.get("model").typing();
    }
  }
});
</script>

And we test it

Since this is also a thing you might be doing a lot of, let’s go through another example.

This time we will try to capture when the user enters / loads the categories page. The categories page is a component. All the components in the Discourse repository live here

https://github.com/discourse/discourse/tree/master/app/assets/javascripts/discourse/components

and we can find the one we’re after here

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/components/discovery-categories.js.es6

so we’re going to need something like this

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("component:discovery-categories", {

});
</script>

If you want to fire scripts when a component is loaded you can use something like this

didInsertElement: function() {
  this._super();
  // do your work here
}

So we end up with this

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("component:discovery-categories", {
  didInsertElement: function() {
    this._super();
    console.log("Welcome to the categories page!")
  }
});
</script>

Now all that’s left is to check and see if it works

et voilà :tada:

We can now move on to doing something similar with widgets.

I mentioned earlier, that we’ll cover widgets and so here’s what you need to know about them before we move on to the next few methods.

import { createWidget } from 'discourse/widgets/widget';

createWidget('my-widget', {
  tagName: 'div.hello',

  html() {
    return "hello world";
  }
});

With this basic understanding, we can move on to the things you can do with widgets. There are three things you can do to widgets in Discourse themes.

  1. Modify them - like we did with controllers and components
  2. Decorate them - as in add elements before or after them
  3. Create them from scratch

Well, it turns out the pluginAPI has a method for each of these. So let’s have a look at those methods, but before we start. Here’s where all the widgets live in the Discourse repository

https://github.com/discourse/discourse/tree/master/app/assets/javascripts/discourse/widgets

- reopenWidget()

Let’s start with the reopen widget method. This method is similar to what we did with controllers and components. I have a bit of a lengthy example, but it does demonstrate how much flexibility the pluginAPI offers you as theme developer. The example is the Alternative Logo theme component

This is a theme component that will allow you to add alternative logos for dark / light themes.

At its heart, this theme only overwrites one of the functions of the home-logo widget

So, we already know the name of the widget we want to reopen. it’s home-logo and so we start with this

<script type="text/discourse-plugin" version="0.8.13">
api.reopenWidget("home-logo", {

});
</script>

then find the function that we want to overwrite in that widget. Here I want to change the logo image so this looks promising.

So we copy that function as is first

Expand snippet
<script type="text/discourse-plugin" version="0.8.13">
api.reopenWidget("home-logo", {
  logo() {
    const { siteSettings } = this;
    const mobileView = this.site.mobileView;

    const mobileLogoUrl = siteSettings.mobile_logo_url || "";
    const showMobileLogo = mobileView && mobileLogoUrl.length > 0;

    const logoUrl = siteSettings.logo_url || "";
    const title = siteSettings.title;

    if (!mobileView && this.attrs.minimized) {
      const logoSmallUrl = siteSettings.logo_small_url || "";
      if (logoSmallUrl.length) {
        return h("img#site-logo.logo-small", {
          key: "logo-small",
          attributes: {
            src: Discourse.getURL(logoSmallUrl),
            width: 33,
            height: 33,
            alt: title
          }
        });
      } else {
        return iconNode("home");
      }
    } else if (showMobileLogo) {
      return h("img#site-logo.logo-big", {
        key: "logo-mobile",
        attributes: { src: Discourse.getURL(mobileLogoUrl), alt: title }
      });
    } else if (logoUrl.length) {
      return h("img#site-logo.logo-big", {
        key: "logo-big",
        attributes: { src: Discourse.getURL(logoUrl), alt: title }
      });
    } else {
      return h("h1#site-text-logo.text-logo", { key: "logo-text" }, title);
    }
  }
});
</script>

And then adjust it according to the desired behaviour. In my case it was like this

Expand snippet
<script type="text/discourse-plugin" version="0.8.13">
api.reopenWidget("home-logo", {
  logo() {
    const { siteSettings } = this,
      { iconNode } = require("discourse/helpers/fa-icon-node"),
      h = require("virtual-dom").h,
      altLogo = settings.Alternative_logo_url,
      altLogoSmall = settings.Alternative_small_logo_url,
      mobileView = this.site.mobileView,
      mobileLogoUrl = siteSettings.mobile_logo_url || "",
      showMobileLogo = mobileView && mobileLogoUrl.length > 0;
    (logoUrl = altLogo || ""),
    (title = siteSettings.title);
    if (!mobileView && this.attrs.minimized) {
      const logoSmallUrl = altLogoSmall || "";
      if (logoSmallUrl.length) {
        return h("img#site-logo.logo-small", {
          key: "logo-small",
          attributes: { src: logoSmallUrl, width: 33, height: 33, alt: title }
        });
      } else {
        return iconNode("home");
      }
    } else if (showMobileLogo) {
      return h("img#site-logo.logo-big", {
        key: "logo-mobile",
        attributes: { src: mobileLogoUrl, alt: title }
      });
    } else if (logoUrl.length) {
      return h("img#site-logo.logo-big", {
        key: "logo-big",
        attributes: { src: logoUrl, alt: title }
      });
    } else {
      return h("h1#site-text-logo.text-logo", { key: "logo-text" }, title);
    }
  }
});
</script>

A quick summary of the changes in the above snippet

  1. Replace the variables for the URL of the logo
  2. Pull the value of those variables from the theme’s settings - we’ll talk about theme settings a bit later on.

You can do the same for any function in any widget using the reopenWidget() pluginAPI method.

We can now move on to another thing you can do to widgets

- decorateWidget()

With this method you will be able to add html before or after a widget. You use it like this

<script type="text/discourse-plugin" version="0.8">
  api.decorateWidget('NAME:LOCATION', helper => {

  });
</script>

Where name is the NAME of the widget and LOCATION is either before or after depending on where you want your HTML to show up - before or after the widget.

So this

<script type="text/discourse-plugin" version="0.8">
  api.decorateWidget('post:after', helper => {
    return helper.h('p', 'Hello');
  });
</script>

Would add <p>Hello</p> after every “post” widget like so

and this

<script type="text/discourse-plugin" version="0.8">
  api.decorateWidget('header-icons:before', helper => {
      return helper.h('li', [
          helper.h('a.icon', {
              href:'https://foo.bar.com/',
              title: 'Foobar'
          }, helper.h('i.fa.fa-heart.d-icon')),
      ]);
  });
</script>

would add

<li>
  <a href="https://foo.bar.com/" title="Foobar" class="icon">
    <i class="fa fa-heart d-icon"></i>
  </a>
</li>

right before the header icons widget.

This leaves us with one last things you can do with widgets. Create them!

- createWidget()

Now that you have a basic understanding of widgets, you’re in a good place to create one! The pluginAPI has a method that makes this super easy.

Here’s a basic example

<script type="text/discourse-plugin" version="0.8">
  const h = require("virtual-dom").h;

  api.createWidget("my-first-widget", {
    tagName: "div.my-widget",

    html() {
      return h('h1', "Hello World!");
    }
  });
</script>

Start by requiring the relevant bit from the virtual dom library

const h = require("virtual-dom").h;

This allows you to add html element in a more efficient way.

h('h1', "Hello World!")

Is the exact same as

<h1>Hello World!</h1>

And while it may look like the same amount of code for one element, it scales a lot better.

For a quick example, this - when set up to loop

return h(
  "li.headerLink." + seg[3] + "." + seg[5],
  helper.h(
    "a",
    {
      href: seg[2],
      title: seg[1],
      target: seg[4]
    },
    seg[0]
  )
);

can produce

Expand snippet
<li class="headerLink vdo">
  <a href="/c/tech" title="Discussions about technology" target="">Tech</a>
</li>
<li class="headerLink vdo">
  <a href="https://www.vat19.com/" title="Buy some cool stuff" target="_blank">Shop</a>
</li>
<li class="headerLink vdo">
   <a href="/t/284" title="Mobile OS poll" target="">Your Vote Counts!</a>
</li>
<li class="headerLink vdo keep">
  <a href="/latest/?order=op_likes" title="Posts with the most amount of likes" target="">Most Liked</a>
</li>
<li class="headerLink vdm keep">
  <a href="/privacy" title="Our Privacy Policy" target="">Privacy</a>
</li>

But is a lot more maintainable

Ok…fine let’s go back to creating a widget. So now we have this

<script type="text/discourse-plugin" version="0.8">
  const h = require("virtual-dom").h;

  api.createWidget("my-first-widget", {
    tagName: "div.my-widget",

    html() {
      return h('h1', "Hello World!");
    }
  });
</script>

Which creates the widget. and if you remember, we can mount widgets in Handlebars template. So we can do something like this

<script type="text/x-handlebars" data-template-name="/connectors/above-footer/inject-widget">
  {{mount-widget widget="my-first-widget"}}
</script>

and add bit of SCSS

@import "common/foundation/variables";

.my-widget {
  display: flex;
  align-items: center;
  justify-content: center;
  background: $primary-high;
  h1 {
  padding: .5em;
  color: $secondary;
  margin: 0;
  }
}

And we’re done, your first Discourse Widget!

Here are a couple more examples that you can look at to see createWidget in action

Brand header theme component:

https://github.com/discourse/discourse-brand-header/blob/master/common/header.html

Discourse Category banners theme component:

https://github.com/awesomerobot/discourse-category-banners/blob/master/common/header.html

This covers creating, modifying and decorating widgets. The three things I said you can do to widget with Discourse themes, but I lied :lying_face:

There’s actually one more thing you can do with some widgets, change their settings

- changeWidgetSetting()

Some widgets like the home-logo or the post-avatar widgets have settings. If a widget has settings, you can easily change those settings with the pluginAPI

For example, the post-avatar widget has these

and the home-logo widget has this

To, change a setting you can do something like this

<script type="text/discourse-plugin" version="0.8">
  api.changeWidgetSetting('WIDGET-NAME', 'SETTING-NAME', 'VALUE');
</script>

So to change the avatar size setting in the post-avatar widget we can use

<script type="text/discourse-plugin" version="0.8">
  api.changeWidgetSetting('post-avatar', 'size', '90');
</script>

and to change the href setting in the home-logo widget we would use

<script type="text/discourse-plugin" version="0.8">
  api.changeWidgetSetting('home-logo', 'href', 'https://some.url.com');
</script>

- addNavigationBarItem()

You use this method to add new items to the navigation bar here

To add a new link you use something like this

<script type="text/discourse-plugin" version="0.8">
  api.addNavigationBarItem({
    name: "link-to-movies-category",
    displayName: "movies",
    href: "/c/movies",
    title: "link title"
  })
</script>

To add a link to the “movies” category in the navigation menu like so

- addUserMenuGlyph()

You use this method to add a new linked icon to the user menu

Here’s an example, let’s say you want to add a link to the users mentions page, you would then use something like

<script type="text/discourse-plugin" version="0.8">
api.addUserMenuGlyph({
  label: 'Mentions',
  className: 'mention-link',
  icon: 'at',
  href: '/my/notifications/mentions'
});
</script>

Which creates a new icon that takes the user to their mentions page when clicked

- onPageChange()

While it’s obviously much better to have your scripts only fire when needed, this method gives you a way to fire scripts once with every page change, here’s a basic example

<script type="text/discourse-plugin" version="0.8">
  api.onPageChange((url, title) => {
    console.log('the page changed to: ' + url + ' and title ' + title);
  });
</script>

This should log the page URL and title to the console on every page change, and if we check the console after navigating to a few pages we see this

- decorateCooked()

Posts in Discourse are widgets, as such, the contents of a post will not be available for you to target with JS or jQuery. Luckily though, the pluginAPI provides you with a method to reach those contents. The basic usage for this method is

<script type="text/discourse-plugin" version="0.8">
  api.decorateCooked($elem => $elem.children('p').addClass('foo-class'));
</script>

This will find all <p> tags in the cooked contet of a post and add the class foo-class to them

That’s the basics of it, but if you need to do something a little bit more complicated, I suggest writing your own jQuery mini plugin like so

var tiles_selector = '.cooked div[data-theme-tiles="1"]';
$.fn.dtiles = function() {
  this.each(function() {
    var update = function() {
      $(this).masonry({
        itemSelector: ".lightbox-wrapper",
        transitionDuration: 0
      });
    };
    this.addEventListener("load", update, true);
  });
  return this;
};

api.decorateCooked($elem =>
  $elem
    .children(tiles_selector)
    .dtiles()
    .addClass("tiles-initialized")
);

This is how the Tiles image gallery theme component works.

- onToolbarCreate()

This is the method that you would use to add new buttons to the composer toolbar.

You would use it like this

<script type="text/discourse-plugin" version="0.8">
  $(document).ready(function() {
    let currentLocale = I18n.currentLocale(); 
    I18n.translations[currentLocale].js.composer.my_button_text = "Hey there!";
    I18n.translations[currentLocale].js.my_button_title = "My Button!";
  });

  api.onToolbarCreate(function(toolbar) {
    toolbar.addButton({
      trimLeading: true,
      id: "buttonID",
      group: "insertions",
      icon: 'heart',
      title: "my_button_title",
      perform: function(e) {
        return e.applySurround(
          '<div data-theme="foo">\n\n',
          "\n\n</div>",
          "my_button_text"
        );
      }
    });
  });
</script>

Note that the translation strings at the top are required and kudos to Simon Cossar for coming up with a way to dynamically setting them based on the user’s locale

To add a button with a heart icon that wraps selected text with <div data-theme="foo"></div>


The pluginAPI has over 40 methods in it and so we’ll stop here because I think the methods above are enough for the purposes of this guide.

However, this doesn’t mean that you should not be aware of them. As mentioned before, you can find all the methods in the pluginAPI here in the Discourse repository

https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/lib/plugin-api.js.es6

4.d. Theme settings

I won’t be covering theme settings in the guide since we already have a very good guide detailing how to use them in your themes.

5. Best practices

Now that you are familiar with Discourse theme basics, let’s move on to a few recommendations that will make your life as a Discourse theme developer a lot easier.

5.a. Use the Discourse Theme CLI

Not long ago, Sam created an amazing tool that will help you create themes more efficiently. You can use the Discourse Theme tool to generate all the files required for a new theme with a couple of lines in the command line. It also allows you to connect your local theme files to a remote Discourse install (like theme creator) and you can see changes you make to those files take effect live on a remote install.

Read more about it here:

5.b. Use Prettier

Prettier is a code formatting tool. The files on the Discourse repository are Prettified for consistent code formatting. It makes your code easier to read and…wait for it…pretty!

You can read more about Prettier here:

5.c. How to ask for help

If you’re stuck on something that relates to themes, feel free to create a post in the #dev category. Be sure to include as much information as you can and anything you’ve tried. The more effort you put into your question, the more likely you’ll get an answer.

You’ll get extra attention if you make it clear that the help you need is for a theme you intend to release here on Meta. If this is the case, feel free to tag me in your question and I’ll go out of my way to try and answer your question so that the theme gets published.

5.d. Include a license

Including a license with your theme makes it clear that it’s intended to be shared. I recommend the MIT license or equivalent for open source themes.

5.e. Creating a topic for your theme on Meta

If you’d like to share your theme here on Meta, please consider including the following if applicable

  1. Short description at the top
  2. Screenshots
  3. Preview on theme creator
  4. Instructions for theme settings
  5. Link to theme installation guide

While we covered a lot of subjects, this guide barely scratches the surface and there’s obviously things I may have missed.

If you have any questions, please don’t hesitate to ask - assuming you’re still alive. :sweat_smile:

This guide is a wiki, if you have any improvements, please go ahead and make them by editing the topic.


Request: House Ads Plugin
Is there any way to hyperlink to the middle of a post?
Stick to the bottom banner - Independent ad option?
What do I need before signing up?
What do you use to make animated/gif instructions?
Theme Creator Web Dev Help
Make Tag Dropdown Retain Focus After Tag Picked; Client-Side API Questions
Customize template used by post widget
Request modification homepage
How would updating effect custom overrides?
Avatars by posts in Category with Featured Topics
Updating Discourse with custom modification
Como faço para entender esse fórum? (How do I understand this forum?)
Insert plain text (or image) anywhere in Discourse
Can a Discourse theme/template be customised to have this format?
Click counter missing for some internal links
Render modal from theme client-side plugin API
Is there an event that fires when topic-lists are reloaded?
Working on first plugin; does not work. Using the pirate-speak plugin as a base
Disable right click on images
Any options for over-riding the username restrictions?
#2

Great guide @Johani. I will examine it carefully. Thanks! :slight_smile:


#3

Where should we add this? In common/head_tag.html?


(Joe) #4

Template modifications require script tags, as such, the same would apply to them

So yes, your options here are

common/head_tag.html
common/header.html
common/body_tag.html

And they would work fine.

However, since this will only affect the desktop template, you can organise your theme even more and add it to one of these instead

desktop/head_tag.html
desktop/header.html
desktop/body_tag.html

#5

:clap::clap::clap:

Incredible work @Johani! Truly a masterpiece!