Embed comments from Discourse in your single page app

When trying to implement [1] in your Single Page Application (SPA) which does not provide server side rendered pages, you will ultimately run into issues, see: [2]

So after a bit of tinkering, I’d like to present the following approach. The example is in Vue.js, however, it can be easily adapted to other frameworks/libs.

Note: I will use the term blog posts where a Discourse comments section should be embedded. But of course this can also mean individual pages on your site.

1. The issues in [1]

1.1. javascripts/embed.js cannot work with client side rendered content

The <script>...</script> snippet you are told in [1] to insert into your HTML will thus not be part of the implementation we are approaching here. We will utilize some bits of javascripts/embed.js provided by your Discourse instance as functions within our SPA.

1.2. Discourse cannot scrape client side rendered content

Discourse automatically creates topics for each blog post and tries to access the the original URL (of a blog post) to determine the title and content. This fails with an SPA, because Discourse will get the non-javascript part of it, e.g. We're sorry but this site doesn't work properly without JavaScript enabled. Please enable it to continue.

We will utilize the RSS Polling plugin to provide the necessary data and create the topics for us.

2. The implemenation

2.1 RSS Polling and the RSS/Atom feed

Create an endpoint on your site which provides an RSS or Atom feed for the RSS Polling plugin. This endpoint can either be just a static XML formatted file or a server-side function providing the XML formatted content, example:

URL: https://mysite.com/blog.atom

Content:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title>My Site Blog Posts</title>
  <link href="https://mysite.com/blog/"/>
  <updated>2022-07-03T09:02:48.721Z</updated>
  <id>urn:uuid:790c1857-b968-49cc-9fbd-bf7afe3552c2</id>

  <entry>
    <title>An Article about Technology</title>
    <author>
      <name>Your Name Here</name>
    </author>
    <link href="https://mysite.com/blog/an-article-about-technology"/>
    <id>urn:uuid:f6cc13e4-d2eb-4385-af28-c867a94f48dc</id>
    <published>2022-07-03T00:00:00Z</published>
    <updated>2022-07-03T00:00:00Z</updated>
    <summary>Let's discuss some technology in this article.</summary>
  </entry>

</feed>

Install the RSS Polling plugin for Discourse as per [3] (Discourse hosted) or [4] (self hosted)

Recommended Settings for RSS Polling in Admin → Settings → Plugins:

Key Value
rss polling enabled true
rss polling frequency 10 (i.e. 10 minutes)

Add a new feed in the RSS Polling plugin configuration at Admin → Plugins → RSS Polling

Configure it as per [3]:

Key Value
URL https://mysite.com/blog.atom
Category Filter <this is optional>
Author <define an author of the automatically generated topics>
Category <define the category/ies where the automatically generated topics will be posted>
Tags <this is optional>

2.2 SPA Router Configuration

Discourse uses the last part of the URL path as the identifier for an individual blog post.

Examples:

https://mysite.com/blog/an-article-about-technology
https://mysite.com/blog/another-article-about-cats

Configure your SPA router accordingly, so that individual blog posts correspond with individual URLs.

Also: Discourse will provide a link back to the individual blog post, so it is good UX that when clicked, your site shows the actual article.

2.3 The Article Component

As told before, we can’t use the approach with the <script></script> from [1]. So we implement the iframe and some functions from javascripts/embed.js in our component:

Article.vue

Things you should edit:

item description
#YOUR-DISCOURSE-URL# the URL of your Discourse instance, e.g. discourse.mysite.com)
#YOUR-SITE-URL# the URL of your site, e.g. mysite.com possibly also %2Fblog%2F if your blog posts’ path is not /blog/
<template>
  <div id="article">

    <!-- your formatted article here -->

    <iframe
      v-if="slug"
      v-bind:src="`https://#YOUR-DISCOURSE-URL#/embed/comments?embed_url=https%3A%2F%2F#YOUR-SITE-URL#%2Fblog%2F${slug}%2F`"
      id="discourse-embed-frame"
      width="100%"
      v-bind:height="`${iframeHeight}px`"
      frameborder="0"
      scrolling="no"
      referrerpolicy="no-referrer-when-downgrade"
    />
  </div>
</template>

<script>
export default {
  data: () => ({
    slug: null,     // the slug of the blog post, e.g. "an-article-about-technology" while the route is "https://mysite.com/blog/an-article-about-technology"
    iframeHeight: 0 // Discourse will tell us the exact iframe height (see: receiveMessage method)
  }),

  methods: {
    // iframe communcation
    receiveMessage(event) {
      if (!event) {
        return;
      }
      if (!(event.origin || "").includes("#YOUR-DISCOURSE-URL#")) {
        return;
      }

      if (event.data) {
        if (event.data.type === "discourse-resize" && event.data.height) {
          this.iframeHeight = +event.data.height;
        }

        if (event.data.type === "discourse-scroll" && event.data.top) {
          // find iframe offset
          const destY = this.findPosY(this.$refs["discourse-embed-frame"]) + event.data.top;
          window.scrollTo(0, destY);
        }
      }
    },

    // Thanks http://amendsoft-javascript.blogspot.ca/2010/04/find-x-and-y-coordinate-of-html-control.html
    findPosY(obj) {
      var top = 0;
      if (obj.offsetParent) {
        while (1) {
          top += obj.offsetTop;
          if (!obj.offsetParent) break;
          obj = obj.offsetParent;
        }
      } else if (obj.y) {
        top += obj.y;
      }
      return top;
    }
  },

  async created() {
    this.slug = this.$router.currentRoute.path.split("/")[2];
  },

  mounted() {
    window.addEventListener("message", this.receiveMessage);
  },
  beforeDestroy() {
    window.removeEventListener("message", this.receiveMessage);
  }
}

2.4 Discourse Embed Configuration

Now with the RSS/Atom Feed polling in place and the implementation on your site, we can finally configure the embedding on the Discourse instance.

Go to Admin → Customize → Embedding and add a host:

Key Value
Allowed Hosts your site base URL> e.g. “mysite.com
Class Name optional class name for styling
Path Allowlist e.g. “/blog/.*”
Post to Category same category as configured in RSS Polling Category

3. Final Remarks

The RSS/Atom feed polling as well as Discourse itself will create a new topic if it does not exist for the individual blog post. Ensure that the RSS/Atom feed polling comes first (i.e. wait until the topic is created before you visit the blog post on your site).

Reason: Discourse can’t scrape the title and summary, so the topic would be mysite.com with the summary being We are sorry, but this site does not work without javascript. :wink:

If for some reason Discourse came first, you can just delete the topic and wait until the RSS/Atom feed kicked in.

cheers

– MK2k

1 Like