Creating Routes in Discourse and Showing Data

Over time Discourse has grown in complexity and it can be daunting for beginners to understand how data gets all the way from the back end Ruby on Rails application to the Ember.js application in front.

This tutorial is meant to show the full lifecycle of a request in Discourse and explain the steps necessary if you want to build a new page with its own URL in our application.

URLs First

I always prefer to start thinking of features in terms of the URLs to access them. For example let’s say we want to build an admin feature that showed the last snack I ate while working on Discourse. A suitable URL for that would be /admin/snack

In this case:

  • Visiting /admin/snack in your browser should show the snack using the “full stack”, in other words the Ember application will be loaded up and it would request the data it needs to display the snack.

  • Visiting /admin/snack.json should return the JSON data for the snack itself.

The Server Side (Ruby on Rails)

Let’s start by creating a new controller for the snack.

app/controllers/admin/snack_controller.rb

class Admin::SnackController < Admin::AdminController

  def index
    render json: { name: "donut", description: "delicious!" }
  end

end

In this case we inherit from Admin::AdminController to gain all the security checks to make sure the user viewing the controller is an administrator. We just have one more thing to do to before we can access our controller, and that’s to add a line to config/routes.rb:

Find the block that looks like this:

namespace :admin, constraints: StaffConstraint.new do
  # lots of stuff
end

And add this line inside it:

get 'snack' => 'snack#index'

Once you’re done, you should be able to visit /admin/snack.json in your browser and you’ll see JSON for the snack! Our snack API seems to be working :candy:

Of course, as you build your feature to add more complexity you likely wouldn’t just return hardcoded JSON from a controller like this, you’d query the database and return it that way.

The Client Side (Ember.js)

If you open up your browser and visit /admin/snack (without the .json) you’ll see that Discourse says “Oops! That page doesn’t exist.” — that’s because there’s nothing in our front end Ember application to respond to the route. Let’s add a handlebars template to show our snack:

app/assets/javascripts/admin/templates/snack.hbs

<h1>{{model.name}}</h1>

<hr>

<p>{{model.description}}</p>

And, like on the Rails API side we need to wire up the route. Open the file app/assets/javascripts/admin/routes/admin-route-map.js.es6 and look for the export default function() method. Add the following line:

this.route('snack');

We have one final thing left to do in Ember land, and that’s to have the Ember application perform an AJAX request to fetch our JSON from the server. Let’s create one last file. This will be an Ember Route. Its model() function will be called when the route is entered, so we’ll make our ajax call in there:

app/assets/javascripts/admin/routes/admin-snack.js.es6

import { ajax } from 'discourse/lib/ajax';

export default Ember.Route.extend({
  model() {
    return ajax('/admin/snack.json');
  }
});

Now, you can open your browser to /admin/snack and you should see the details of the snack rendered in the page!

Summary

  • Opening your browser to /admin/snack boots up the Ember application

  • The Ember application router says snack should be the route

  • The Ember.Route for snack makes an AJAX request to /admin/snack.json

  • The Rails application router says that should be the admin_snack controller

  • The admin_snack_controller returns JSON

  • The Ember application gets the JSON and renders the Handlebars template

Where to go from here

I’ve written a follow up tutorial on how to add an Ember Component to Discourse.

40 Likes

I looked for “map” in the admin-route-map.js.es6 file but to no avail.

So I added it near the end like so.


    this.route('adminPlugins', { path: '/plugins', resetNamespace: true }, function() {
      this.route('index', { path: '/' });
    });

    this.route('snack');

  });
};

It could use some CSS love, but it worked!

2 Likes

Thanks for letting me know! I changed the API slightly since this tutorial was written. If a map is not exporting under a particular resource it can just export a function. You figured out the correct solution :slight_smile:

(I’ve also fixed the OP)

5 Likes

Slight correction, that should actually be one layer deeper :slight_smile:

export default function() {
  this.route('admin', { resetNamespace: true }, function() {
    this.route('snack');
1 Like

If we’re adding new URLs should it be done in Discourse itself or in a plugin? If I edit Discoure’s code, won’t that make the forum software difficult to update?

Yes, it would be good to have a version of this post from a plug-in perspective, showing end to end flow and the different files for a very similar example. Trying to get my head round this at this very moment!

4 Likes

Sounds like you’re looking for this guide:

4 Likes

Gah, of course, thanks - I was put off by the title thinking ‘I don’t need an Admin Interfaces for this project so i’ll look at this later’ … ooops

1 Like

Sorry for bumping an old topic, but using this discourse AJAX call how do we access data from the Rails server at a URL which needs to be dynamically defined?

eg
https://discourse.baseurl.org/u/:username/something.json

On the Ember side, something like:

ajax("/u/${username}", { type: 'GET' }).then((result) => { etc.

(replace the speechmarks with backticks and make sure you’ve included import { ajax } from "discourse/lib/ajax"; above)

On the Ruby side, something like:

get '/u/:username' => 'something#index'

I think this will be addressable as /u/username.json (which is not exactly what you asked for)

you might try:

get '/u/:username/something' => 'something#index'

if you insist on the something bit.

Can’t guarantee that’s 100% but may help you move forward …

This is, as always, a good reference:

3 Likes

Thanks @merefield

I’m OK on the Rails-side routing. I have to say it’s very hard to make progress on anything but trivial plugins, without better documentation of the Discourse-specific parts of the API, since not all of it is pure Ember or pure Rails. I’m all for reading the source code to work stuff out, but equally it would be nice if there were just some comments in the source to explain the API of some common components, how to invoke the Composer, etc.

5 Likes

Here is a similar guide by @kleinfreund which shows in great depth, how build discourse plugins. Great resource.

5 Likes

I’d love to see this re-written as a sample to add routes in a plugin. I’m sure that it’s “easy,” but it’s taking me a while to make the subtle transformations.

The Beginner's Guide to Creating Discourse Plugins Part 5: Admin Interfaces is a start, but there are still some missing pieces to actually adding a route with a new model.

1 Like

I was very excited to see the guide you linked (now 2 years old), but it doesn’t work. I thought it was me messing up creating my version with my plugin name, but then I cloned his code and it gives me a “Oops!” for /notebook.

It gets “Ooops” rather than “route not found”, so it’s doing something close. . . but I’m back to being lost. I’m sure it’s one line of code, or something in the wrong place, but I’m stuck.

1 Like

It was working a few months ago for sure. I’ll take a look maybe in the weekend. Bookmarking it for now. :wink:

3 Likes

@pfaffman were you able to figure out what the problem was? I have the same issue with the guide

2 Likes

I was not. It’s like to get it figured out, but the pressing issue I was looking to solve I may solve without discourse.

The notebook tutorial had an error due to this issue:

Which I fixed and tested with the PR:

https://github.com/kleinfreund/notebook/pull/2/files

and it works again:

Hope this helps.

PS: Slowly, I’m making progress with learning to write Discourse plugins… very, very slowly :slight_smile: LOL

3 Likes

Thank you @neounix

Much appreciated. Tried that plugin tutorial months ago and could not get it to work and gave up.

Now, it works fine with that code change:

cat notebook.js.es6 
/**
 * Route for the path `/notebook` as defined in `../notebook-route-map.js.es6`.
 */
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
  renderTemplate() {
    // Renders the template `../templates/notebook.hbs`
    this.render("notebook");
  },
});

Thanks again!

This will help a lot of wannabes like me.

1 Like

Awesome! Thanks @neounix! I figured that it was a one line fix, but had no clue how to figure out what that one line was.

:tada:

Now I can get back to my Discourse Server Manager plugin that I can hopefully use to build a GitHub App to install, upgrade, and manage plugins in Discourse installations.

2 Likes