# Developing Discourse Plugins - Part 1 - Create a basic plugin

**URL:** https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515
**Category:** Developer Guides
**Tags:** plugin-guides, tutorial
**Created:** [June 26, 2015, 9:01pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515 "2015-06-26T21:01:08Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![Discourse](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/discourse/32/148734_2.png) [@Discourse](https://meta.discourse.org/u/Discourse)
#### Post date: [June 26, 2015, 9:01pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/1 "2015-06-26T21:01:08Z")

</div>

Building a plugin in Discourse can be really simple, once you learn a couple of quirks. The goal of this post is to create a skeleton plugin and introduce you to the basics.

### Your development environment

Make sure you have a development environment of Discourse running on your computer. I recommend you use [the appropriate setup guide](https://meta.discourse.org/tag/dev-install) and come back when you’re done.

### plugin.rb

> :tada: Use [GitHub - discourse/discourse-plugin-skeleton: Template for Discourse plugins · GitHub](https://github.com/discourse/discourse-plugin-skeleton) to create a complete discourse plugin skeleton in your plugins directory :tada:

> The skeleton is now bundled in discourse core, `rake plugin:create[plugin-name]` will create a plugin using the skeleton

When Discourse starts up, it looks in the `plugins` directory for subdirectories containing a `plugin.rb` file. The `plugin.rb` file has two purposes: it is the manifest for your plugin with the required information about your plugin including: its name, contact information and a description. The second purpose is to initialize any ruby code necessary to run your plugin.

In our case, we won’t be adding any ruby code but we still need the `plugin.rb`. Let’s create the directory `basic-plugin` with the file `plugin.rb` inside it, with the following contents:

### basic-plugin/plugin.rb

```rb
# name: basic-plugin
# about: A super simple plugin to demonstrate how plugins work
# version: 0.0.1
# authors: Awesome Plugin Developer
# url: https://github.com/yourusername/basic-plugin

```

Once you’ve created this file, you should restart your local server and the plugin should be loaded.

### An important Gotcha!

If you’re used to regular rails development you might notice that plugins aren’t quite as nice when it comes to reloading. In general, when you make changes to your plugin, you should Ctrl+c the server to stop it running, then run it again using `bin/dev`.

### My changes weren’t picked up! :warning:

Sometimes the cache isn’t cleared fully, especially when you create new files or delete old files. To get around this issue, remove your `tmp` folder and start rails again. On a mac you can do it in one command: `rm -rf tmp; bin/dev`.

### Checking that your plugin was loaded

Once you’ve restarted your local server, visit the url `/admin/plugins` (make sure you’re [logged in as an admin account](https://meta.discourse.org/t/create-admin-account-from-console/17274) first, as only admins can see the plugin registry).

If everything worked, you should see your plugin in the list:

 ![basic-plugin shown in the admin plugin list](https://global.discourse-cdn.com/meta/original/4X/9/7/b/97b7e2daac4cae0c6b3642a17d5b87dcec4b5c73.png)

Congratulations, you just created your first plugin!

### Let’s add some Javascript

Right now your plugin doesn’t do anything. Let’s add a javascript file that will pop up an alert box when discourse loads. This will be super annoying to any user and is not recommended as an actual plugin, but will show how to insert Javascript into our running application.

Create the following file:

### `plugins/basic-plugin/assets/javascripts/discourse/initializers/alert.js`

```js
export default {
  name: "alert",
  initialize() {
    alert("alert boxes are annoying!");
  },
};

```

Now if you restart your local server, you should see “alert boxes are annoying!” appear on the screen. (If you did not, see the “My Changes weren’t picked up” heading above).

Let’s step through how this worked:

1. Javascript files placed in `assets/javascripts/discourse/initializers` are executed automatically when the Discourse application loads up.

2. This particular file `export`s one object, which has a `name` and an `initialize` function.

3. The `name` has to be unique, so I just called it `alert`.

4. The `initialize()` function is called when the application loads. In our case, all it does is execute our `alert()` code.

You’re now an official Discourse plugin developer!

* * *

### More in the series

**Part 1: This topic**  
Part 2: [Plugin Outlets](https://meta.discourse.org/t/beginners-guide-to-creating-discourse-plugins-part-2-plugin-outlets/31001)  
Part 3: [Site Settings](https://meta.discourse.org/t/beginners-guide-to-creating-discourse-plugins-part-3-custom-settings/31115)  
Part 4: [git setup](https://meta.discourse.org/t/beginners-guide-to-creating-discourse-plugins-part-4-git-setup/31272)  
Part 5: [Admin interfaces](https://meta.discourse.org/t/beginners-guide-to-creating-discourse-plugins-part-5-admin-interfaces/31761)  
Part 6: [Acceptance tests](https://meta.discourse.org/t/beginner-s-guide-to-creating-discourse-plugins-part-6-acceptance-tests/32619)  
Part 7: [Publish your plugin](https://meta.discourse.org/t/beginner-s-guide-to-creating-discourse-plugins-part-7-publish-your-plugin/101636)

* * *

This document is version controlled - suggest changes [on github](https://github.com/discourse/discourse/blob/main/docs/developer-guides/docs/04-plugins/01-basic-plugin.md).

---

<div class="post-metadata">

### Author: ![Ishan\_Dutta](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/ishan_dutta/32/121571_2.png) [@Ishan\_Dutta](https://meta.discourse.org/u/Ishan_Dutta)
#### Post date: [August 30, 2016, 2:09am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/45 "2016-08-30T02:09:42Z")

</div>

I am able to create plugin in development, it works fine. Thanks.  
But I don’t see a way to install other’s plugin in development  
So first question is how can I fork someone’s plugin in development and then build on top of that(hope copy paste that repo is not the recommended way)?  
Second question is, it seems my created plugin is part of my repo, is there a way to create a separate github repo for the plugin automatically which others can directly use to install?

---

<div class="post-metadata">

### Author: ![eviltrout](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/eviltrout/32/5275_2.png) [@eviltrout](https://meta.discourse.org/u/eviltrout)
#### Post date: [August 30, 2016, 2:11pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/46 "2016-08-30T14:11:18Z")

</div>

I have a preferred method to do this. First, I have a `~/code` directory where I have all my git projects. So I check out discourse and the plugin in that directory so it’s something like this:

**~/code**

```plaintext
discourse
discourse-some-plugin

```

Then, within the `~/code/discourse/plugins` folder I create a symlink to the plugin:

```bash
$ cd ~/code/discourse/plugins
$ ln -s ~/code/discourse-some-plugin .

```

Then for good measure:

```bash
$ cd ~/code/discourse
$ rm -rf tmp
$ bundle exec rails server

```

Now you can fork the plugin in `~/code/discourse-some-plugin`, make pull requests or whatever you want. It’ll be used by discourse.

---

<div class="post-metadata">

### Author: ![Jithin\_Krishnan](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jithin_krishnan/32/121329_2.png) [@Jithin\_Krishnan](https://meta.discourse.org/u/Jithin_Krishnan)
#### Post date: [December 26, 2016, 4:47am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/77 "2016-12-26T04:47:17Z")

</div>

What is the best way to render ajax response inside a widget?  
I was able to `console.log` the ajax response, but it is not rendering inside the widget.

Tried to call `this.scheduleRerender()` after getting ajax response but both results in an infinite loop.

---

<div class="post-metadata">

### Author: ![Jithin\_Krishnan](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jithin_krishnan/32/121329_2.png) [@Jithin\_Krishnan](https://meta.discourse.org/u/Jithin_Krishnan)
#### Post date: [December 27, 2016, 5:20am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/78 "2016-12-27T05:20:28Z")

</div>

I am getting following error when trying to load a helper module inside my widget

 ![](https://global.discourse-cdn.com/meta/original/3X/5/c/5cb2748e4650c0b03d3259ed2536292f8cd2f2cb.png)

Could you please help me to resolve this error?  
Thanks

---

<div class="post-metadata">

### Author: ![eviltrout](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/eviltrout/32/5275_2.png) [@eviltrout](https://meta.discourse.org/u/eviltrout)
#### Post date: [December 27, 2016, 4:07pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/79 "2016-12-27T16:07:45Z")

</div>

> [@Jithin\_Krishnan](#):
>
> What is the best way to render ajax response inside a widget?

You should make sure your widget can contain state. After the ajax request, set it on the state object and trigger a `this.scheduleRerender` and it should appear. For an example look at how the [post menu](https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/widgets/post-menu.js.es6#L183) shows who liked something.

> [@Jithin\_Krishnan](#):
>
> I am getting following error when trying to load a helper module inside my widget

Looks like you have the wrong path to your helper. One way to see all the paths that Discourse has resolved is by typing `require._eak_seen` in your console. Look for the correct path name.

---

<div class="post-metadata">

### Author: ![Jithin\_Krishnan](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jithin_krishnan/32/121329_2.png) [@Jithin\_Krishnan](https://meta.discourse.org/u/Jithin_Krishnan)
#### Post date: [December 29, 2016, 7:11am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/80 "2016-12-29T07:11:56Z")

</div>

Thanks Robin Ward for your help. I was able to load my module after investigating with `require._eak_seen`.

Below is the code for my widget. The problem is that `this.scheduleRerender()` is causing an infinite loop. The div always shows loading animation (even without `this.scheduleRerender`) which seems `this.state.loading` is not being set.

```
import { createWidget } from 'discourse/widgets/widget';
import { getTopic } from 'discourse/plugins/my-plugin/discourse/helpers/topics';
import { ajax } from 'discourse/lib/ajax';
import { h } from 'virtual-dom';

export default createWidget('topic-widget', {
  tagName: 'div.my-topics',
  defaultState() {
    return { loading: false};
  },

  refreshTopic() {
    if (this.state.loading) { return; }
    this.state.loading = true;
    this.state.topic = 'empty';
    getTopic(this).then((result) => {
      console.log(result);
      this.state.topic = result;
      this.state.loading = false;
      this.scheduleRerender();
    });
  },

  html(attrs, state) {
    if (!state.topic) {
      this.refreshTopic();
    }
    const result = [];
    if (state.loading) {
      result.push(h('div.spinner-container', h('div.spinner')));
    } else if (state.topic !== 'empty') {
      result.push(state.topic);
    } else {
      result.push(h('div.no-messages', 'No topic.'))
    }

    return result;
  },
});

```

Could you help to resolve this error?

---

<div class="post-metadata">

### Author: ![eviltrout](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/eviltrout/32/5275_2.png) [@eviltrout](https://meta.discourse.org/u/eviltrout)
#### Post date: [December 29, 2016, 3:51pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/81 "2016-12-29T15:51:37Z")

</div>

Your code looks mostly fine, however, all widgets that deal with state require a `key` attribute. You should have seen a warning about this, although maybe the warning only appears when running tests?

Try adding a `buildKey` function like `buildKey: () => 'topic-widget'`.

That should probably fix it. Also, a much less serious issue is you might want to add `topic: null` to your state object. Javascript is much faster when it [knows the shape of the object](https://ariya.io/2012/02/javascript-object-structure-speed-matters) in advance.

---

<div class="post-metadata">

### Author: ![Jithin\_Krishnan](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jithin_krishnan/32/121329_2.png) [@Jithin\_Krishnan](https://meta.discourse.org/u/Jithin_Krishnan)
#### Post date: [December 30, 2016, 5:05am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/82 "2016-12-30T05:05:52Z")

</div>

Thanks Robin Ward.  
Adding buildKey function worked fine for me.

---

<div class="post-metadata">

### Author: ![tgxworld](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/tgxworld/32/106117_2.png) [@tgxworld](https://meta.discourse.org/u/tgxworld)
#### Post date: [January 27, 2017, 8:29am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/83 "2017-01-27T08:29:19Z")

</div>

> [@Jithin\_Krishnan](#):
>
> The problem is that this.scheduleRerender() is causing an infinite loop.

> [@eviltrout](#):
>
> Your code looks mostly fine, however, all widgets that deal with state require a key attribute. You should have seen a warning about this, although maybe the warning only appears when running tests?

@eviltrout I got bitten by this in development and didn’t get a warning about setting a key attribute.

---

<div class="post-metadata">

### Author: ![eviltrout](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/eviltrout/32/5275_2.png) [@eviltrout](https://meta.discourse.org/u/eviltrout)
#### Post date: [January 27, 2017, 4:42pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/84 "2017-01-27T16:42:12Z")

</div>

Ah, it only warns in `Ember.testing`:

[https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/widgets/widget.js.es6#L153](https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/widgets/widget.js.es6#L153)

We might want to just raise an error if that happens now? It is pretty dangerous for Widgets to do that.

---

<div class="post-metadata">

### Author: ![tgxworld](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/tgxworld/32/106117_2.png) [@tgxworld](https://meta.discourse.org/u/tgxworld)
#### Post date: [January 28, 2017, 3:06am UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/85 "2017-01-28T03:06:21Z")

</div>

:thumbsup: for raising an error instead. Maybe we can extend that to development too? :thought_balloon:

---

<div class="post-metadata">

### Author: ![eviltrout](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/eviltrout/32/5275_2.png) [@eviltrout](https://meta.discourse.org/u/eviltrout)
#### Post date: [January 30, 2017, 5:04pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/86 "2017-01-30T17:04:21Z")

</div>

Hopefully this will prevent others from making this mistake:

[https://github.com/discourse/discourse/commit/d4bbdcd7d63ddbf20239c95d1ba1cee9030d7e95](https://github.com/discourse/discourse/commit/d4bbdcd7d63ddbf20239c95d1ba1cee9030d7e95)

---

<div class="post-metadata">

### Author: ![Sudaraka](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sudaraka/32/68401_2.png) [@Sudaraka](https://meta.discourse.org/u/Sudaraka)
#### Post date: [March 25, 2017, 3:13pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/87 "2017-03-25T15:13:32Z")

</div>

Hi,

> [@eviltrout](#):
>
> but will show how to insert Javascript into our running application.
> 
> Create the following file:
> 
> plugins/basic-plugin/assets/javascripts/discourse/initializers/alert.js.es6

Is this url correct? Shouldn’t this be ,

> [@](#):
>
> plugins/basic-plugin/assets/javascripts/initializers/alert.js.es6

I’m new here. please correct me if im wrong :slight_smile:

---

<div class="post-metadata">

### Author: ![vinothkannans](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/vinothkannans/32/86465_2.png) [@vinothkannans](https://meta.discourse.org/u/vinothkannans)
#### Post date: [March 25, 2017, 3:34pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/88 "2017-03-25T15:34:49Z")

</div>

In Discourse, mainly there are two big JavaScript sections “discourse” and “admin”. Also you can see few more in [https://github.com/discourse/discourse/tree/master/app/assets/javascripts](https://github.com/discourse/discourse/tree/master/app/assets/javascripts).

In plugins, to differentiate admin & normal user section JavaScript files we use “discourse” and “admin” keywords in between like below

> plugins/basic-plugin/assets/javascripts/discourse/initializers/alert.js.es6  
> plugins/basic-plugin/assets/javascripts/admin/initializers/alert.js.es6

* * *

> [@Sudaraka](#):
>
> plugins/basic-plugin/assets/javascripts/initializers/alert.js.es6

Also it will just work even without identification keywords like you mentioned :slight_smile:

---

<div class="post-metadata">

### Author: ![Sudaraka](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sudaraka/32/68401_2.png) [@Sudaraka](https://meta.discourse.org/u/Sudaraka)
#### Post date: [March 25, 2017, 3:38pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/89 "2017-03-25T15:38:07Z")

</div>

@vinothkannans Thank you a lot for the explanation. :slight_smile:

---

<div class="post-metadata">

### Author: ![pacharanero](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pacharanero/32/500583_2.png) [@pacharanero](https://meta.discourse.org/u/pacharanero)
#### Post date: [March 20, 2019, 9:07pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/105 "2019-03-20T21:07:34Z")

</div>

> [@clay](#):
>
> I was having trouble with Discourse seeing the plugin when I used a symlink (on MacOS High Sierra). I deleted the symlink and moved the plugin folder into the Discourse hierarchy and everything worked as expected. Maybe this will help others who run into a similar issue.

I’m experiencing the same issue with getting symlinks to work (I’m on Linux) - the `ln -s` appears to work, but from inside the docker container the path is inaccessible. The plugin works when it is directly copied into the plugins folder, but I like the symlink workflow as it enables a more sensible Git arrangment.

This SO question appears to suggest that symlinks won’t work this way unless we also add the linked-to plugin volume to the `docker run` command [https://stackoverflow.com/questions/38485607/mount-host-directory-with-a-symbolic-link-inside-in-docker-container](https://stackoverflow.com/questions/38485607/mount-host-directory-with-a-symbolic-link-inside-in-docker-container)

Anyone any thoughts on this? What are the pro plugin developers doing for a sensible Git workflow?

---

<div class="post-metadata">

### Author: ![merefield](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/merefield/32/176214_2.png) [@merefield](https://meta.discourse.org/u/merefield)
#### Post date: [March 20, 2019, 9:12pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/106 "2019-03-20T21:12:03Z")

</div>

I’m using symlinks but my dev setup is Docker-frei.

Symlinks are a really nice way of being able to include or exclude plugins on a ‘build’ very quickly without disturbing the codebase.

---

<div class="post-metadata">

### Author: ![pacharanero](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/pacharanero/32/500583_2.png) [@pacharanero](https://meta.discourse.org/u/pacharanero)
#### Post date: [April 3, 2019, 4:14pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/107 "2019-04-03T16:14:55Z")

</div>

> [@pacharanero](#):
>
> I’m experiencing the same issue with getting symlinks to work (I’m on Linux) - the `ln -s` appears to work, but from inside the docker container the path is inaccessible. The plugin works when it is directly copied into the plugins folder, but I like the symlink workflow as it enables a more sensible Git arrangment.

Here’s what I’ve had to do in order to preserve a similar workflow to using symlinks (softlinks):

I edited `/bin/docker/boot_dev` in Discourse (outside the container) so that the plugin I want to work on is added as a volume and mounted in the `/src/plugins/` directory (inside the container).

In my case it looks like this:

```bash
docker run -d -p 9405:9405 -p 1080:1080 -p 3000:3000 -p 9292:9292 \
-v "$DATA_DIR:/shared/postgres_data:delegated" \
-v "$SOURCE_DIR:/src:delegated" \ 
-v "/home/marcus/code/discourse/discourse-reflective-learning-plugin:/src/plugins/discourse-reflective-learning-plugin" \
$ENV_ARGS --hostname=discourse --name=discourse_dev --restart=always \
discourse/discourse_dev:release /sbin/boot

```

I’ve used absolute paths because I couldn’t be bothered to get into relative paths inside Docker and `$SOURCE_DIR` environment variables, but I’m sure there’s a cleverer way to do this, so that perhaps all your plugins could be in directories at the same level as `discourse` and it would automagically include them all. Hope this helps someone.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [April 3, 2019, 10:35pm UTC](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515/108 "2019-04-03T22:35:34Z")

</div>

> [@pacharanero](#):
>
> I edited `/bin/docker/boot_dev` in Discourse

I am totally open to a PR that automatically follows symlinks in `plugins/` dir and then smart mounts volumes.

[Next page](https://meta.discourse.org/t/developing-discourse-plugins-part-1-create-a-basic-plugin/30515.md?page=2)
