FEATURE: Embed topics list on remote sites via Javascript API. (#8008)

FEATURE: Embed topics list on remote sites via Javascript API. (#8008)

This adds support for a <d-topics-list> tag you can embed in your site that will be rendered as a list of discourse topics. Any attributes on the tag will be passed as filters. For example:

<d-topics-list discourse-url="URL" category="1234"> will filter to category 1234.

To use this feature, enable the embed topics list site setting. Then on the site you want to embed, include the following javascript:

<script src="http://URL/javascripts/embed-topics.js"></script>

Where URL is your discourse forum’s URL.

Then include the <d-topics-list discourse-url="URL"> tag in your HTML document and it will be replaced with the list of topics.

diff --git a/app/assets/javascripts/embed-application.js.no-module.es6 b/app/assets/javascripts/embed-application.js.no-module.es6
index f8d9c08..9cdda82 100644
--- a/app/assets/javascripts/embed-application.js.no-module.es6
+++ b/app/assets/javascripts/embed-application.js.no-module.es6
@@ -24,17 +24,20 @@
 
   window.onload = function() {
     // get state info from data attribute
-    var header = document.querySelector("header");
+    var embedState = document.querySelector("[data-embed-state]");
     var state = "unknown";
-    if (header) {
-      state = header.getAttribute("data-embed-state");
+    var embedId = null;
+    if (embedState) {
+      state = embedState.getAttribute("data-embed-state");
+      embedId = embedState.getAttribute("data-embed-id");
     }
 
     // Send a post message with our loaded height and state
     postUp({
       type: "discourse-resize",
       height: document["body"].offsetHeight,
-      state: state
+      state,
+      embedId
     });
 
     var postLinks = document.querySelectorAll("a[data-link-to-post]"),
diff --git a/app/assets/stylesheets/embed.scss b/app/assets/stylesheets/embed.scss
index a48d0ac..6caaa12 100644
--- a/app/assets/stylesheets/embed.scss
+++ b/app/assets/stylesheets/embed.scss
@@ -175,3 +175,19 @@ aside.onebox {
 div.lightbox-wrapper {
   margin-bottom: 20px;
 }
+
+.topics-list {
+  width: 100%;
+  .topic-list-item {
+    td {
+      padding: 0.5rem;
+    }
+
+    .main-link a {
+      color: $primary;
+    }
+    .main-link a:visited {
+      color: $primary-medium;
+    }
+  }
+}
diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb
index 759e334..93c9417 100644
--- a/app/controllers/embed_controller.rb
+++ b/app/controllers/embed_controller.rb
@@ -1,10 +1,12 @@
 # frozen_string_literal: true
 
 class EmbedController < ApplicationController
+  include TopicQueryParams
+
   skip_before_action :check_xhr, :preload_json, :verify_authenticity_token
 
-  before_action :ensure_embeddable, except: [ :info ]
-  before_action :get_embeddable_css_class, except: [ :info ]
+  before_action :ensure_embeddable, except: [ :info, :topics ]
+  before_action :get_embeddable_css_class, except: [ :info, :topics ]
   before_action :ensure_api_request, only: [ :info ]
 
   layout 'embed'
@@ -16,7 +18,26 @@ class EmbedController < ApplicationController
       @show_reason = true
       @hosts = EmbeddableHost.all
     end
-    render 'embed_error'
+    render 'embed_error', status: 400
+  end
+
+  def topics
+    discourse_expires_in 1.minute
+
+    response.headers['X-Frame-Options'] = "ALLOWALL"
+    unless SiteSetting.embed_topics_list?
+      render 'embed_topics_error', status: 400
+      return
+    end
+
+    if @embed_id = params[:discourse_embed_id]
+      raise Discourse::InvalidParameters.new(:embed_id) unless @embed_id =~ /^de\-[a-zA-Z0-9]+$/
+    end
+
+    list_options = build_topic_list_options
+    list_options[:per_page] = params[:per_page].to_i if params.has_key?(:per_page)
+    topic_query = TopicQuery.new(current_user, list_options)
+    @list = topic_query.list_latest
   end
 
   def comments
diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb
index 69a6c96..6d2bace 100644
--- a/app/controllers/list_controller.rb
+++ b/app/controllers/list_controller.rb
@@ -1,9 +1,11 @@
 # frozen_string_literal: true
 
 require_dependency 'topic_list_responder'
+require_dependency 'topic_query_params'
 
 class ListController < ApplicationController
   include TopicListResponder
+  include TopicQueryParams
 
   skip_before_action :check_xhr
 
@@ -376,28 +378,6 @@ class ListController < ApplicationController
     end
   end
 
-  def build_topic_list_options
-    options = {}
-    params[:tags] = [params[:tag_id].parameterize] if params[:tag_id].present? && guardian.can_tag_pms?
-
-    TopicQuery.public_valid_options.each do |key|
-      if params.key?(key)
-        val = options[key] = params[key]
-        if !TopicQuery.validate?(key, val)
-          raise Discourse::InvalidParameters.new key
-        end
-      end
-    end
-
-    # hacky columns get special handling
-    options[:topic_ids] = param_to_integer_list(:topic_ids)
-    if options[:no_subcategories] == 'true'
-      options[:no_subcategories] = true
-    end
-
-    options
-  end
-
   def list_target_user
     if params[:user_id] && guardian.is_staff?
       User.find(params[:user_id].to_i)
diff --git a/app/views/embed/embed_topics_error.html.erb b/app/views/embed/embed_topics_error.html.erb
new file mode 100644
index 0000000..392e36e
--- /dev/null
+++ b/app/views/embed/embed_topics_error.html.erb
@@ -0,0 +1,8 @@
+<header class='discourse' data-embed-state='error'>
+  <h3><%= t 'embed.error' %></h3>
+  <%= link_to(image_tag(SiteSetting.site_logo_url, class: 'logo'), Discourse.base_url) %>
+  <div class='clearfix'></div>
+</header>
+<div class='embed-error'>
+  <%= t('embed.error_topics') %>
+</div>
diff --git a/app/views/embed/topics.html.erb b/app/views/embed/topics.html.erb
new file mode 100644
index 0000000..6f478ee
--- /dev/null
+++ b/app/views/embed/topics.html.erb
@@ -0,0 +1,11 @@
+<%- if @list && @list.topics.present? %>
+  <table class='topics-list' data-embed-state='loaded' <%- if @embed_id %>data-embed-id="<%= @embed_id %>"<%- end %>>
+    <%- @list.topics.each do |t| %>
+      <tr class='topic-list-item'>
+        <td class='main-link'>
+          <a target="_parent" href="<%= t.url %>" class="title raw-link raw-topic-link" data-topic-id="<%= t.id %>"><%= t.title %></a>
+        </td>
+      </div>
+    <%- end %>
+  </table>
+<%- end %>
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index a3553d7..4dc3a02 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -268,6 +268,7 @@ en:
     continue: "Continue Discussion"
     error: "Error Embedding"
     referer: "Referer:"
+    error_topics: "The `embed topics list` site setting was not enabled"
     mismatch: "The referer was either not sent, or did not match any of the following hosts:"
     no_hosts: "No hosts were set up for embedding."
     configure: "Configure Embedding"
@@ -1943,6 +1944,7 @@ en:
     autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language."
     highlighted_languages: "Included syntax highlighting rules. (Warning: including too many languages may impact performance) see: <a href='https://highlightjs.org/static/demo/' target='_blank'>https://highlightjs.org/static/demo</a> for a demo"
 
+    embed_topics_list: "Support HTML embedding of topics lists"
     embed_truncate: "Truncate the embedded posts."
     embed_support_markdown: "Support Markdown formatting for embedded posts."
     embed_whitelist_selector: "A comma separated list of CSS elements that are allowed in embeds."
diff --git a/config/routes.rb b/config/routes.rb
index 88db857..2ef2345 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -711,6 +711,7 @@ Discourse::Application.routes.draw do
     end
   end
 
+  get 'embed/topics' => 'embed#topics'
   get 'embed/comments' => 'embed#comments'
   get 'embed/count' => 'embed#count'
   get 'embed/info' => 'embed#info'
diff --git a/config/site_settings.yml b/config/site_settings.yml
index cfaf971..1f1c1ee 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -849,6 +849,7 @@ posting:
     choices:
       - 4-spaces-indent
       - code-fences
+  embed_topics_list: false
   embed_truncate:
     default: true
   embed_support_markdown:
diff --git a/lib/topic_query_params.rb b/lib/topic_query_params.rb
new file mode 100644
index 0000000..e1bb23c
--- /dev/null
+++ b/lib/topic_query_params.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module TopicQueryParams
+  def build_topic_list_options
+    options = {}
+    params[:tags] = [params[:tag_id].parameterize] if params[:tag_id].present? && guardian.can_tag_pms?
+
+    TopicQuery.public_valid_options.each do |key|
+      if params.key?(key)

[... diff too long, it was truncated ...]

GitHub sha: 23367e79

1 Like

This commit has been mentioned on Discourse Meta. There might be relevant details there: