How to add Giscus (GitHub Discussions) comments to an Astro blog
A static blog has no server, so the usual answer to “how do I add comments?” is “sign up for a hosted comment service.” Giscus is a nicer answer: it’s a free, open-source widget that stores every comment as a GitHub Discussion on a repo you own. No database, no backend, no accounts to manage — your readers sign in with GitHub, and the comments live in your repo.
This is exactly how comments work on this blog. Let me walk through how it’s wired up, end to end.
What Giscus actually is
Giscus is a <script> you drop onto a page. When it loads, it renders an
<iframe> that talks to the GitHub Discussions API. Each blog post maps to one
Discussion; each reply is a comment on that Discussion. Because everything is
stored on GitHub, you get moderation, reactions, and Markdown for free, and you
never store user data yourself.
The one-time setup happens on GitHub and on the giscus site — the code in your repo just points at the result.
Step 1: Prepare the GitHub repo
Three things have to be true on the repo that will hold the comments:
- The repo is public.
- Discussions are enabled (repo Settings → Features → Discussions).
- The giscus GitHub App is installed on that repo (from
github.com/apps/giscus).
Step 2: Get your repo-id and category-id
Go to giscus.app, type in your owner/repo, and pick a
Discussions category (a general one like “Announcements” works well). The page
generates a config snippet for you. The two values you must copy out are:
data-repo-id— a stable id for the repodata-category-id— a stable id for the category
These are not secrets, but they are the two pieces the widget can’t guess on its own.
Step 3: Put the config in one place
This blog keeps all the Giscus values in a single typed constant, so the component that renders the widget stays clean. Here’s the real config:
// blog/src/consts.ts
export const GISCUS = {
repo: "developerehsan/developerEhsan-portfolio", // owner/repo
repoId: "REPLACE_ME_FROM_GISCUS_APP", // data-repo-id from giscus.app
category: "Announcements", // Discussions category name
categoryId: "REPLACE_ME_FROM_GISCUS_APP", // data-category-id from giscus.app
mapping: "pathname",
theme: "dark_dimmed",
} as constA couple of fields worth explaining:
mapping: "pathname"tells Giscus how to decide which Discussion belongs to which page."pathname"means “one Discussion per URL path” — so/posts/my-postgets its own thread. This is the option you usually want.themeis a Giscus theme name."dark_dimmed"matches this blog’s dark terminal look. More on themes below.
The REPLACE_ME_FROM_GISCUS_APP placeholders are the signal that setup isn’t
finished yet. That matters for the next step.
Step 4: The component
Here’s the actual Giscus.astro component. Read it top to bottom — it’s short.
---
// blog/src/components/Giscus.astro
import { GISCUS } from "@/consts"
const configured =
GISCUS.repoId !== "REPLACE_ME_FROM_GISCUS_APP" &&
GISCUS.categoryId !== "REPLACE_ME_FROM_GISCUS_APP"
---
<section aria-label="Comments" class="mt-12">
<div class="mb-4 flex items-center gap-2 font-mono text-xs text-[oklch(0.6_0_0)]">
<span class="font-bold text-[oklch(0.6_0.1_150)]">❯</span>
<span>cat ./comments</span>
</div>
{
configured ? (
<div class="giscus"></div>
) : (
<p class="terminal-card p-4 font-mono text-xs text-[oklch(0.62_0_0)]">
Comments are not configured yet. Enable GitHub Discussions and paste the
giscus repo-id / category-id into <code>src/consts.ts</code>.
</p>
)
}
</section>The configured check is the nice part. If the ids are still placeholders, the
component renders a friendly note instead of a broken widget — no error, no
half-loaded iframe. Once you paste real ids into consts.ts, configured
flips to true and the real container renders.
Step 5: The script embed
The <div class="giscus"> above is just a mounting point. The widget is
actually loaded by Giscus’s client script, which this blog renders only when
configured is true:
{
configured && (
<script
is:inline
src="https://giscus.app/client.js"
data-repo={GISCUS.repo}
data-repo-id={GISCUS.repoId}
data-category={GISCUS.category}
data-category-id={GISCUS.categoryId}
data-mapping={GISCUS.mapping}
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="top"
data-theme={GISCUS.theme}
data-lang="en"
data-loading="lazy"
crossorigin="anonymous"
async
/>
)
}The important attributes here:
is:inlineis an Astro instruction. Normally Astro processes and bundles<script>tags;is:inlinetells it to leave this one exactly as written so the Giscus loader runs as-is with all itsdata-*attributes intact.data-loading="lazy"defers loading the iframe until it’s near the viewport, so comments don’t slow down the initial page.data-reactions-enabled="1"shows the 👍/❤️ reaction bar.data-input-position="top"puts the comment box above the thread.
Every data-* value comes straight from the GISCUS object, so the widget and
your config can never drift apart.
Matching the dark theme
The blog is dark-only, so a bright default comment box would stick out. Two things keep Giscus on-brand:
- The
theme: "dark_dimmed"value inconsts.ts— Giscus ships several built-in dark themes, and this one has muted, low-contrast surfaces that fit the blog’s OKLCH terminal palette. If you want a perfect match you can also pointdata-themeat a full CSS URL, but a built-in dark theme is the easy win. - The wrapper markup uses the blog’s own tokens (
terminal-card, the mono font, the❯ cat ./commentsprompt) so the section around the widget reads like the rest of the site.
What to remember
- Giscus turns GitHub Discussions into a comment section — no backend, no database, and comments live in a repo you control.
- The one-time setup is on GitHub (public repo + Discussions + the giscus app) and on giscus.app (copy the repo-id and category-id).
- Keep the config in one typed constant and gate rendering on whether the ids are still placeholders, so an unconfigured widget degrades gracefully.
- Use
is:inlineso Astro doesn’t rewrite the Giscus loader script. - Match your site by picking a built-in dark theme like
dark_dimmed.
Related reading: comments are one of a few small client-side add-ons on this blog — see also privacy-friendly analytics with a tiny Astro component.