Embeds Stuck on “Loading Discussion…”? Topic Not Created? Here's the Fix

Over the course of a few days I noticed a lot of people have searched and posted for solutions concerning comment embeds. I am one of those people. I hope this post helps others in the same situation.

I am new to Discourse, so anyone wishing to complement the information I am providing with in depth expertise please feel free to do so.

One thing I can say after reviewing the many posts on the topic is that the source of the issues can be wide ranging. For those in my situation, here is a solution!

Problem

  1. You find yourself with an embed that says “Loading discussion…”
  2. Discourse topics are not being automatically created

Solution

Try adding your domain to the list of allowed internal hosts.

It’s a site setting found in the admin area. You can find them at this path of your Discourse site:

/admin/site_settings/category/all_results

A direct link to the setting I am referencing would be:

/admin/site_settings/category/all_results?filter=allowed_internal_hosts

For those of you looking at the Rails console, look at:

SiteSetting.allowed_internal_hosts

The setting is a pipe (|) separated list of domain names.

Context

My Discourse instance is public, but my internal DNS resolves some domains locally. This can happen in setups using Docker, Kubernetes, or any environment with internal DNS.

Being new to Discourse, I must say that what seems obvious now, was really not obvious to begin with.

Those of us who are not familiar with Discourse internals are not aware that in 2017 SSRF protection was implemented or even the specifics of that protection. Only in hindsight does that announcement make the connection clear.

It is a well-implemented feature, but was quite the rabbit hole for a very simple reason.

What you must know

Discourse won’t create a topic for your embed if the domain resolves to a local IP.

Don’t scream yet folks. This is a good thing. You can read about SSRF to find out why and also thank Discourse devs for taking it seriously.

The issue is that Discourse does not provide feedback to let us know why it’s not creating the topics and why it’s stuck at “loading discussion…”

Additional Reading

But, what is a local IP exactly? For anyone interested, you can find the answer right in the Discourse code, here is a direct link to the file on GitHub.

For example, if your Discourse instance at super-forum[dot]com lives on a network that also hosts cool-blog[dot]net, your internal DNS might resolve cool-blog[dot]net as a local IP—which Discourse will reject unless it’s allowlisted.

Hopefully this post saves someone else a few hours of head-scratching—and maybe even a few hairs.

2 Likes

As I got back to work today, a few things stood stood out to me in the admin pages. Here are a few thoughts on what could be improved.

Embedding Settings — /admin/customize/embedding/settings

allowed_internal_hosts is a crucial setting for embedding to work reliably in non-public environments. It should be explicitly listed as a related setting in this section — there’s no question about its importance.

Embedding Hosts — /admin/customize/embedding

The provided configuration snippet is more than helpful as includes a lot of useful information. I think we can use that to provide additional guidance.

First paragraph

Current:

Paste the following HTML code into your site to create and embed Discourse topics. Replace EMBED_URL with the canonical URL of the page you are embedding it on.

Alternative:

Paste the following HTML where you want the comments to appear on your page.
The discourseEmbedUrl is the URL of your page—the one that will be linked from Discourse. When your page is first loaded, Discourse will attempt to find or create a topic for that URL and link back to your content.

Second paragraph

Current:

If you want to customize the style, uncomment and replace CLASS_NAME with a CSS class defined in the Embedded CSS of your theme.

Alternative:

Use the className property to add custom classes to the <html> tag inside the embedded iframe. To style it, go to /admin/customize/themes, click your theme’s Edit button, then the Edit Code button, and check Show Advanced. Add your custom CSS to the Embedded CSS section.

Third paragraph

Current:

Replace DISCOURSE_USERNAME with the Discourse username of the author that should create the topic. Discourse will automatically lookup the user by the content attribute of the <meta> tags with name attribute set to discourse-username or author. The discourseUserName parameter has been deprecated and will be removed in Discourse 3.2.

Alternative:

Note: The topic is created by a real Discourse user—not a display name or author string. It must be a valid, existing account. There are three ways to determine which user is used:

  1. Default fallback—set in /admin/customize/embedding/posts_and_topics
  2. Per-host override—set under /admin/customize/embedding/
  3. Per-URL control—add a <meta name="discourse-username" content="USERNAME"> tag to your page with an existing Discourse USERNAME

Only the username of an existing Discourse user will work. Discourse will fall back to the host-level or global default if the meta tag user isn’t found. The <meta> tag method illustrated here allows for programmatic, per-URL control over which Discourse user is used to create the topic. For example, you can map blog post authors on your site to matching Discourse user accounts.

The Configuration Snippet section

The collapsible “Configuration Snippet” section is easy to miss. Visually, it resembles a heading, and the subtle arrow isn’t intuitive. Unlike the “Learn More” link, which is colored and eye-catching, this one feels hidden in plain sight.

I know this might be debatable—some may feel the UI is clean and sufficient. But I personally missed this section far too many times before realizing it was clickable. That tells me the affordance could be improved, even if only slightly. A clearer visual cue or a non-collapsed default may go a long way, especially for newcomers who rely on examples.

The Snippet

Current:

<div id='discourse-comments'></div>
  <meta name='discourse-username' content='DISCOURSE_USERNAME'>

  <script type="text/javascript">
    DiscourseEmbed = {
      discourseUrl: 'https://discourse.your-site.com/',
      discourseEmbedUrl: 'EMBED_URL',
      // className: 'CLASS_NAME',
    };

    (function() {
      var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true;
      d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
      (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d);
    })();
  </script>

Alternative:

<div id="discourse-comments"></div>
<!-- Optional: specify which Discourse user account creates the topic -->
<!-- If omitted, Discourse falls back to the per-host or global default user -->
<meta name="discourse-username" content="DISCOURSE_USERNAME" />

<script type="text/javascript">
  DiscourseEmbed = {
    discourseUrl: 'https://discourse.mydomain.com/', // Trailing slash required
    discourseEmbedUrl: window.location.href, // Or a hardcoded canonical URL string
    // className: 'my-iframe-theme another-class',
    // discourseReferrerPolicy: 'strict-origin-when-cross-origin',
    // topicId: '1234',
  };

  (function() {
    const d = document.createElement('script');
    d.type = 'text/javascript';
    d.async = true;
    d.src = `${DiscourseEmbed.discourseUrl}javascripts/embed.js`;
    document.head.appendChild(d);
  })();
</script>

DiscourseEmbed options—short descriptions

  • discourseUrlRequired
    The full URL of your Discourse instance. Must end with a trailing slash, e.g. https://discourse.mydomain.com/

  • discourseEmbedUrlRequired
    The full URL of the current page where the comments are being embedded. This is how Discourse identifies and links topics to your content.

  • classNameOptional
    Adds custom CSS classes to the <html> element within the iframe. Define styles in the “Embedded CSS” section of your Discourse theme.

  • discourseReferrerPolicyOptional
    Defaults to no-referrer-when-downgrade. See: Referrer-Policy

  • topicIdOptional
    If set, Discourse will use this topic directly. Otherwise, it will look for a topic matching discourseEmbedUrl, or create one if none exists.

DiscourseEmbed contextual documentation

The discourseEmbedUrl must be accessible to your Discourse server. When the iframe is loaded, Discourse fetches the page at that URL to create or locate the topic. This works seamlessly for websites hosted on public platforms.

However, if you’re developing locally, the embed may appear stuck at “Loading Discussion…” or not appear at all. This is because local development URLs (like localhost) can be blocked by Discourse’s SSRF protection, or force_https option, or a missing embeddable host.

If you’re building new features for an existing site, one workaround is to point discourseEmbedUrl to the production URL. When embed_any_origin is enabled, Discourse will allow the embed to function even if the iframe is served from a different origin. Comments will load if they exist, or a “Continue Discussion” button will be shown.

Alternatively, if your local domain (e.g. localhost) is added under Embedding Hosts, you may not need embed_any_origin at all. But you still need to add localhost as an embeddable host.

:warning: One caveat: if the force_https setting is enabled, and your development site doesn’t use TLS, embedding will fail. In this case, either disable force_https during development or consider spinning up a separate Discourse instance for testing.

Note: If discourseEmbedUrl is publicly accessible and the embed still shows “Loading Discussion…” without creating a topic, your domain may be blocked by Discourse’s SSRF protection.

This often happens when your Discourse instance is running in an environment with local DNS resolution—such as Docker, Kubernetes, or a LAN with an internal DNS server. In these cases, Discourse may resolve your site’s domain to a local IP address (e.g. 127.0.0.1 or 192.168.x.x) and treat it as unsafe.

To allow access, add your domain to the allowed_internal_hosts site setting. This explicitly marks your domain as safe to fetch, bypassing SSRF filtering.

The full list of blocked IP ranges is available in Discourse’s source code.

Allowed Internal Hosts—Site Setting Description

Current:

A list of internal hosts that discourse can safely crawl for oneboxing and other purposes

Alternative:

Allows Discourse to crawl hosts that resolve to internal IPs. Needed if your site runs behind local DNS (e.g. Docker, LAN, Kubernetes). Required for comment embeds, topic creation, and oneboxing when SSRF protection would otherwise block access.

Considerations

Some of these suggestions may be better suited as links to official documentation. In fact, this post alone may do the job, since it should be indexed like any other. Others may warrant a proper pull request—which I may eventually get around to, just not today.

That said, there may be technical inaccuracies in what I’ve written. Most of it comes from hands-on experience, but I could have misinterpreted a behavior, been fooled by caching (oh, caching…), or simply overlooked something. On that front, I defer to the seasoned Discourse veterans.