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

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.