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.
If for some reason Discourse came first, you can just delete the topic and wait until the RSS/Atom feed kicked in.
cheers
– MK2k