Embed Latest Post Teasers Into Your Website

We needed to dynamically load content from our Discourse forum at labs.daemon.com.au into our company’s public web site at www.daemon.com.au/labs

The code retrieves the latest content in selected categories from Discourse and then wraps it around the markup that works with our website before injecting it into the page. We then add a few more changes to make the code more generic so it can be easily reused on different websites for loading content from different Discourse forums.

A Discourse forum possesses numerous data endpoints. For example, when you go to Latest, it loads the latest.json endpoint that returns the data needed for that particular page. This allows us to display content in Discourse on our own website.

Before we start

In order to load content from Discourse remotely, we have to make Discourse’s endpoints available to our website. This can be done in Discourse’s “Admin” settings.

Log in Discourse using an account with admin access, then go to the “Settings” tab in the “Admin” panel:

Find “Security” in the left hand navigation, then locate the “cors origins” field on the right hand side. Add the URL of the website that will display content from Discourse in the field (in our case: http://www.daemon.com.au/), then save the changes:


Since Discourse generates countless data endpoints, it is important to find the right one depending on what content is required to be displayed remotely. Adding /l/latest.json at the end of the URL of a category page will show the endpoint containing the latest posts for that particular category. For example, https://labs.daemon.com.au/c/design/l/latest.json is the endpoint for https://labs.daemon.com.au/c/design.

HTML & JavaScript

Since we now have the endpoint we needs, we now move to enable our site to read the endpoint so that it can retrieve useful information and display it correctly. In this example, we aim to display the latest 3 posts published by either User #1, #2 or #3 from the “Design” category in the #div on our site. In addition, we do not want to show the “About the design category” post.

N.B. In our example, we use default Bootstrap v4.0.0-beta.2 to provide some necessary styles for demo purposes only and its use is totally optional.

<!DOCTYPE html>
<html lang="en">
    <meta charset="utf-8">
    <meta content="ie=edge" http-equiv="x-ua-compatible">
    <meta content="initial-scale=1.0, shrink-to-fit=no, width=device-width" name="viewport">
    <title>Discourse embed</title>

    <!-- Bootstrap CSS for basic styles in the demo -->
    <link crossorigin="anonymous" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" rel="stylesheet">
    <div class="container">
      <div class="card-deck" id="div"></div>

    <!-- jQuery -->
    <script crossorigin="anonymous" integrity="sha384-p7RDedFtQzvcp0/3247fDud39nqze/MUmahi6MOWjyr3WKWaMOyqhXuCT1sM9Q+l" src="https://code.jquery.com/jquery-3.2.1.js"></script>

    <!-- JavaScript -->
      (function ($) {
        'use strict'

        $(function () {
          $.ajax('https://labs.daemon.com.au/c/design/l/latest.json').then(function (result) {
            // Parse data to generate content from Discourse:
            // * Discourse endpoint, i.e. `result`,
            // * Number of posts to be shown on your site, e.g. `3`,
            // * Optional array of user IDs (whitelist)
            //   if only posts published by particular users are intended to be shown on your site, e.g. `[1, 2, 3]`.
            $('#div').discourse(result, 3, [1, 2, 3]);

          $.fn.discourse = function (feed, numToShow, whitelist) {
            var feedLength = feed.topic_list.topics.length;

            // Make sure there are enough posts to be shown.
            if (numToShow > feedLength) {
              numToShow = feedLength;

            for (var i = 0; i < numToShow; i++) {
              var content = '';

              // URLs in Discourse endpoints are all relative URLs (e.g. topic.image_url),
              // we need this so that the links displayed on your site point to the right places.
              // Please modify this to the URL of your Discourse forum.
              var discourseURL = 'http://labs.daemon.com.au/';

              // Variables for Discourse post data.
              var post          = feed.topic_list.topics[i],
                  postAuthor    = post.posters[0].user_id,
                  postDate      = new Date(post.created_at),
                  postLink      = discourseURL + 't/' + post.slug + '/' + post.id,
                  postThumbnail = discourseURL + post.image_url;

              // If whitelist is present, check whether the post author is a verified user.
              if (typeof whitelist !== 'undefined') {
                var verifiedUser = false;

                for (var n = 0; n < whitelist.length; n++) {
                  if (postAuthor === whitelist[n]) {
                    verifiedUser = true;

                // If the post author is not on the whitelist,
                // breaks this iteration and continues with the next iteration in the loop.
                if (!verifiedUser) {
                  // Increase number of posts to be shown if possible
                  // to compensate for the eliminated post.
                  if (numToShow < feedLength) {


              // The following block of code is optional.
              // The purpose is to ignore the "About the X category" post
              // since it may not be desirable to be displayed on your site.
              if (post.title.substring(0, 10) === "About the " && post.title.substring(post.title.length - 9) === ' category') {
                // Increase number of posts to be shown if possible
                // to compensate for the eliminated post.
                if (numToShow < feedLength) {


              // If a post does not have a thumbnail,
              // then use a default placeholder image as its thumbnail for your site.
              // Please modify this to use your site's placeholder image.
              if (post.image_url === null) {
                postThumbnail = 'http://placehold.it/320x180';

              // Generate HTML for your site.
              // This part of the code may need to be modified accordingly
              // to fit the markup of your site.
              content += '<div class="card" style="max-width: 20rem;">';
                content += '<img alt="' + post.fancy_title + '" class="card-img-top" src="' + postThumbnail + '">';
                content += '<div class="card-body">';
                  content += '<h4 class="card-title">' + post.fancy_title + '</h4>';
                  content += '<p class="card-text"><small>' + postDate.getDate() + '/' + postDate.getMonth() + '/' + postDate.getFullYear() + '</small></p>';
                  // The following line does a little bit more than displaying excerpt as it is,
                  // it replaces `<a>` tags in the excerpt with `<em>` tags
                  // so that they do not show up as links on your site.
                  // This is optional, however it may be necessary under certain circumstances. 
                  content += '<p class="card-text">' + post.excerpt.replace(/<a/g, '<em').replace(/<\/a/g, '</em') + '</p>';
                  content += '<a href="' + postLink + '">Read more</a>';
                content += '</div>';
              content += '</div>';


The final look

The JavaScript is generic enough to be reused on different websites. However, you may want to go through step by step to tailor it to your needs. Especially the HTML markup part as it’s very likely that will need to be customised to suit the markup of your site.


h/t @sesemaya Embed latest topics from Discourse on your website - development - Daemon Labs


I have to say thanks for this how-to; it was very helpful in getting started on our own implementation and I wanted to share our version back with the community for future reference. I slightly refactored your code to better fit our needs and we’re using Mustache templates to handle the markup more flexibly.

Our actual implementation is rather different, but I’ve published a pen of your original demo built with our model:

The included API key is for a temporary TL0 user to enable access as our instance has login required while it’s under development