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.
ThediscourseEmbedUrl
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 withname
attribute set todiscourse-username
orauthor
. ThediscourseUserName
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:
- Default fallback—set in /admin/customize/embedding/posts_and_topics
- Per-host override—set under /admin/customize/embedding/
- Per-URL control—add a
<meta name="discourse-username" content="USERNAME">
tag to your page with an existing DiscourseUSERNAME
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
discourseUrl
— Required
The full URL of your Discourse instance. Must end with a trailing slash, e.g.https://discourse.mydomain.com/
discourseEmbedUrl
— Required
The full URL of the current page where the comments are being embedded. This is how Discourse identifies and links topics to your content.
className
— Optional
Adds custom CSS classes to the<html>
element within the iframe. Define styles in the “Embedded CSS” section of your Discourse theme.
discourseReferrerPolicy
— Optional
Defaults tono-referrer-when-downgrade
. See: Referrer-Policy
topicId
— Optional
If set, Discourse will use this topic directly. Otherwise, it will look for a topic matchingdiscourseEmbedUrl
, 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, orforce_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. Whenembed_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 needembed_any_origin
at all. But you still need to addlocalhost
as an embeddable host.
One caveat: if the
force_https
setting is enabled, and your development site doesn’t use TLS, embedding will fail. In this case, either disableforce_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
or192.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.