I moved away from Blogger in October 2016 and had no comment capability for a while. I added Disqus in May 2020, and this post is a retrospective of how I implemented it.
My goals were pretty specific: keep comments optional per post, hide threads on unpublished or future posts in production, and avoid blowing a hole in my security policy. None of those are hard requirements in isolation, but getting all three to hold together cleanly took more thought than I expected.
This assumes a Gatsby site with posts rendered from Markdown and/or MDX templates.
The site no longer uses Disqus, so the snippets below are representative of that 2020 setup rather than a copy of the current template.
Getting Disqus into the project
The package itself is the easy part:
npm install disqus-react
disqus-react ships a DiscussionEmbed component that handles the embed lifecycle. The interesting decisions all happen around it, not inside it.
Giving each post its own comments switch
The first thing I wanted was a way to disable comments on specific posts without any routing gymnastics. A single allowComments flag in frontmatter turned out to be all I needed.1
---
title: Example Post
path: /blog/2026/04/example
date: 2026-04-12
draft: false
allowComments: true
---
Minimal excerpt for comment gating; full posts also include fields like tags, featuredImageUrl, and attribution.
Setting it to false — or just omitting it — keeps the embed from rendering at all. Nothing to override, nothing to hide with CSS.
I pull it into the post template via GraphQL alongside the fields I already needed:
frontmatter {
title
path
date
publishedAt: date(formatString: "YYYY-MM-DD")
allowComments
}
The gating logic
This is where I spent the most time. My first draft checked allowComments and stopped there, which worked locally but fell over once I had future-dated posts in the pipeline. A static build will happily render comment threads before the post is supposed to be public if you let it.
The fix was to layer in a publish-date check alongside the environment check:
const isProductionBuild = process.env.NODE_ENV === 'production';
const toTimestamp = (rawDate) => {
if (!rawDate) return Number.NaN;
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(rawDate) ? `${rawDate}T00:00:00Z` : rawDate;
const parsed = Date.parse(normalized);
return Number.isNaN(parsed) ? Number.NaN : parsed;
};
const nowTimestamp = Date.now();
const isCurrentPostPublished = toTimestamp(frontmatter.publishedAt) <= nowTimestamp;
const shouldShowComments =
frontmatter.allowComments === true && (!isProductionBuild || isCurrentPostPublished);
In development, shouldShowComments is true whenever allowComments is set — which keeps testing straightforward. In production, a post also has to have passed its publish date. Draft posts don't reach this point; they're filtered out earlier during page creation.
The toTimestamp helper exists because bare YYYY-MM-DD strings are parsed as UTC midnight by Date.parse in some environments and as local midnight in others. Appending T00:00:00Z makes it consistent.
Rendering the embed with stable identifiers
Once shouldShowComments is true, rendering is straightforward:
import { DiscussionEmbed } from 'disqus-react';
const canonicalUrl = `https://marcsantos.com${frontmatter.path}`;
{
shouldShowComments && (
<DiscussionEmbed
shortname="marcsantos"
config={{
identifier: frontmatter.path,
url: canonicalUrl,
title: frontmatter.title,
}}
/>
);
}
The identifier field is worth treating carefully. Disqus uses it to key the thread — change it and the comment history looks gone (it isn't, but recovering it is annoying). I used the post path because I treat slugs as permanent, and it stays stable even if I restructure URLs or add aliases later.
On the url field: if you serve the site from multiple domains or alias domains, always build this from one canonical hostname. Otherwise the same post can accumulate separate threads on each domain.
The CSP problem
I thought I was done after the embed rendered locally. Then I deployed and got a blank space where the comments should have been, plus a cluster of CSP violations in the console that I had completely missed during local testing.
Disqus needs entry in four directives: script-src, connect-src, style-src, and frame-src.2 Missing any one of them produces a different failure mode — the iframe might render but look broken, or the scripts might load but API calls fail silently.
Content-Security-Policy:
script-src 'self' ... https://disqus.com https://*.disqus.com https://disquscdn.com https://*.disquscdn.com;
connect-src 'self' ... https://disqus.com https://*.disqus.com https://disquscdn.com https://*.disquscdn.com;
style-src 'self' ... https://disqus.com https://*.disqus.com https://disquscdn.com https://*.disquscdn.com;
frame-src 'self' ... https://disqus.com https://*.disqus.com https://disquscdn.com https://*.disquscdn.com;
If you hit "taking longer than usual" or a permanently empty embed in production, open the console first. It's almost always CSP.
What I'd watch out for
A few things that caught me — none of them obvious until they weren't working:
The most subtle one is a missing allowComments field in the GraphQL query. If the field isn't requested, the condition fails silently and comments just don't appear. No error, no warning, nothing to trace. I caught it because I noticed a post that should have had comments didn't, and spent longer than I'd like to admit before checking the query.
Unstable identifiers are the other one worth calling out separately. If you rename a post's slug or restructure its path after people have already commented on it, those threads won't show up under the new identifier. Disqus has a URL mapper tool to migrate them, but it's a manual process. Better to pick a stable key from the start.
Inconsistent publish gating can also bite you during a rebuild: if the gating logic isn't exactly the same across every template that renders a post, you can end up with comments visible on content that wasn't supposed to be public yet.
Where this ended up
The Disqus implementation held up fine. The embed itself wires up quickly; most of the work is in the gating and the CSP, and once those are solid it stays low-maintenance.
But I eventually moved off it. I migrated to Giscus — which backs comments with GitHub Discussions — because it matched what I actually wanted better: no ads, data I could see and own directly, and a workflow I was already in. The tradeoff is that commenting requires a GitHub account, which narrows the audience. For this site that felt like the right call — anyone likely to leave a comment here is probably already on GitHub.
The Disqus chapter was worth it. It got comments onto the site and showed me what I wanted from the next version. That was enough.
