Skip to main content
Disqus in Gatsby featured

Disqus in Gatsby

April 12, 2026

6 min read
DevelopmentMeta
Photo by Duy Pham

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.

Footnotes

  1. Frontmatter is the metadata block at the top of a Markdown/MDX file (between --- lines) used to configure the post.

  2. CSP (Content Security Policy) is a browser security rule set that controls which external scripts, frames, styles, and connections are allowed.

Marc Santos

Marc Santos

Full-Stack Engineer & Product Developer

I write about building things—from site features to developer tooling—alongside travel, photography, and occasional personal reflections.

When I’m not building, I’m exploring—whether that’s a new place with a camera, a mountainside, or somewhere underwater.

About MarcBuild a product together

Part 2 of 2

Continue the series

Next: Disqus to Giscus in Gatsby

Continue Reading →

Keep reading

Disqus to Giscus in Gatsby

Disqus to Giscus in Gatsby

May 24, 2026

Masonry Grid Implementation

Masonry Grid Implementation

May 10, 2026

Beat Docs Drift

Beat Docs Drift

Mar 29, 2026