Giscus is one of those tools I actually like recommending. It gives you comments powered by GitHub Discussions, doesn’t drag in a giant ad-tech mess, and feels pretty sane compared to legacy comment widgets.
But the moment you run a strict Content Security Policy, Giscus is also one of those integrations that suddenly stops working with a blank box and a few ugly console errors.
The good news: Giscus is CSP-friendly enough if you know what it loads and where it loads it from.
What Giscus actually does
Giscus is embedded as a script that creates an iframe. That detail matters because CSP treats the parent page, the script, and the iframe as separate things.
A typical embed looks like this:
<script
src="https://giscus.app/client.js"
data-repo="owner/repo"
data-repo-id="R_kgDOExample"
data-category="Announcements"
data-category-id="DIC_kwDOExample4CSD"
data-mapping="pathname"
data-strict="0"
data-reactions-enabled="1"
data-emit-metadata="0"
data-input-position="bottom"
data-theme="preferred_color_scheme"
data-lang="en"
crossorigin="anonymous"
async>
</script>
From a CSP perspective, this usually means:
script-srcmust allowhttps://giscus.appframe-srcmust allowhttps://giscus.appconnect-srcmay need to allow GitHub-related endpoints used inside the embedded app- if you use a nonce-based policy, the inline or external script loading pattern matters
Most breakage happens because people allow the script but forget the iframe.
The minimum CSP for Giscus
If your site already has a baseline CSP, the smallest change is usually:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://giscus.app;
frame-src https://giscus.app;
connect-src 'self' https://giscus.app https://api.github.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
base-uri 'self';
object-src 'none';
This is not a universal perfect policy, but it’s a practical starting point.
A couple of notes:
frame-src https://giscus.appis required because Giscus renders in an iframe.connect-src https://api.github.comis often needed because the widget talks to GitHub APIs.style-src 'unsafe-inline'may already exist on your site. If not, test before adding it just for Giscus. I try not to widen style policy unless I can confirm it’s necessary.
If you want ready-made policy patterns to compare against, csp-examples.com is handy.
A stricter policy using nonces
If you run a nonce-based CSP, you should nonce the Giscus script tag instead of relaxing script-src more than necessary.
Example in a server-rendered template:
<script
nonce="{{ .CSPNonce }}"
src="https://giscus.app/client.js"
data-repo="owner/repo"
data-repo-id="R_kgDOExample"
data-category="Announcements"
data-category-id="DIC_kwDOExample4CSD"
data-mapping="pathname"
data-theme="preferred_color_scheme"
crossorigin="anonymous"
async>
</script>
Then the header:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123' https://giscus.app;
frame-src https://giscus.app;
connect-src 'self' https://api.github.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
base-uri 'self';
object-src 'none';
If you already use 'strict-dynamic', be deliberate. It changes how host allowlists are interpreted in modern browsers. I’ve seen teams assume a hostname in script-src is still doing the work when the nonce and strict-dynamic are actually driving execution.
For example, this real policy from headertest.com shows that pattern clearly:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-M2VhOTMzZGItNzZhYi00M2M2LWI4NjctNjE5Mjg3Nzc1Mzg1' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
If you adapt a policy like that for Giscus, don’t just bolt on giscus.app to script-src and call it done. You also need to think about frame-src and connect-src.
A realistic policy update for an existing site
Say your current site policy looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{nonce}}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
frame-ancestors 'none';
To support Giscus, I’d change it to:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{nonce}}' 'strict-dynamic' https://giscus.app;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.github.com https://giscus.app;
frame-src 'self' https://giscus.app;
base-uri 'self';
form-action 'self';
object-src 'none';
frame-ancestors 'none';
That gives Giscus room to operate without blowing open the rest of the page.
Would I love to avoid 'unsafe-inline' in style-src? Yes. Do I sometimes keep it temporarily while validating a third-party widget? Also yes. Security work on real sites is often incremental.
Adding Giscus in Hugo
Since this is for a Hugo audience, here’s a partial you can drop into layouts/partials/giscus.html:
{{ if .Site.Params.giscus.enabled }}
<script
src="https://giscus.app/client.js"
data-repo="{{ .Site.Params.giscus.repo }}"
data-repo-id="{{ .Site.Params.giscus.repoId }}"
data-category="{{ .Site.Params.giscus.category }}"
data-category-id="{{ .Site.Params.giscus.categoryId }}"
data-mapping="{{ .Site.Params.giscus.mapping | default "pathname" }}"
data-strict="{{ .Site.Params.giscus.strict | default "0" }}"
data-reactions-enabled="{{ .Site.Params.giscus.reactionsEnabled | default "1" }}"
data-emit-metadata="{{ .Site.Params.giscus.emitMetadata | default "0" }}"
data-input-position="{{ .Site.Params.giscus.inputPosition | default "bottom" }}"
data-theme="{{ .Site.Params.giscus.theme | default "preferred_color_scheme" }}"
data-lang="{{ .Site.Params.giscus.lang | default "en" }}"
crossorigin="anonymous"
async>
</script>
{{ end }}
And in hugo.toml:
[params.giscus]
enabled = true
repo = "owner/repo"
repoId = "R_kgDOExample"
category = "Announcements"
categoryId = "DIC_kwDOExample4CSD"
mapping = "pathname"
strict = "0"
reactionsEnabled = "1"
emitMetadata = "0"
inputPosition = "bottom"
theme = "preferred_color_scheme"
lang = "en"
Then include it in your single post template:
<article>
{{ .Content }}
</article>
<section class="comments">
{{ partial "giscus.html" . }}
</section>
If you generate a nonce per request, pass it into the partial and add nonce="{{ .CSPNonce }}" to the script tag.
Common CSP errors and what they actually mean
1. Refused to load the script
You’ll see something like:
Refused to load the script 'https://giscus.app/client.js' because it violates the following Content Security Policy directive: "script-src ..."
Fix:
- add
https://giscus.apptoscript-src - or nonce the script tag correctly
- if using hashes, remember the external script URL still needs to be allowed
2. Refused to frame
Usually:
Refused to frame 'https://giscus.app/' because it violates the following Content Security Policy directive: "frame-src ..."
Fix:
frame-src 'self' https://giscus.app;
This is the one people miss constantly.
3. Refused to connect
Something like:
Refused to connect to 'https://api.github.com/...' because it violates the following Content Security Policy directive: "connect-src ..."
Fix:
connect-src 'self' https://api.github.com https://giscus.app;
I usually allow both api.github.com and giscus.app when testing, then tighten if logs show one is unnecessary.
Use Report-Only first
If you’re deploying CSP changes on a live site, don’t guess. Start with Content-Security-Policy-Report-Only.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{{nonce}}' 'strict-dynamic' https://giscus.app;
frame-src 'self' https://giscus.app;
connect-src 'self' https://api.github.com https://giscus.app;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
Then load a page with comments, open DevTools, and watch what gets blocked.
This is much better than shipping a “secure” policy that silently kills your comments section.
My recommended baseline for Giscus
If you want the short version, this is the policy shape I’d start with for a normal static site using Giscus:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://giscus.app;
frame-src 'self' https://giscus.app;
connect-src 'self' https://api.github.com https://giscus.app;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
And for a stricter nonce-based setup:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{nonce}}' 'strict-dynamic' https://giscus.app;
frame-src 'self' https://giscus.app;
connect-src 'self' https://api.github.com https://giscus.app;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That’s tight enough to be useful and practical enough to survive contact with a real production site.
If Giscus still fails after this, check three things first:
- the script tag is actually present in the rendered HTML
frame-srcincludeshttps://giscus.appconnect-srcallows GitHub API traffic
Nine times out of ten, that’s where the problem is.