Skip to content

Add it to your site

The integration is two lines of HTML and works anywhere. What differs between generators is where you put them, and how you avoid pasting your Worker’s address into forty templates.

<div id="charcha"></div>
<script src="https://your-worker.workers.dev/embed.js" defer></script>

Everything below is that, wrapped in whatever your generator calls a partial.

Write a component once and forget it exists. This is the version worth using, because the address lives in one file and your templates never mention it.

src/components/Comments.astro
---
interface Props {
/** Pin the conversation to a slug instead of the page path. Optional. */
thread?: string
}
const { thread } = Astro.props
const api = import.meta.env.PUBLIC_CHARCHA_API
const sitekey = import.meta.env.PUBLIC_TURNSTILE_SITEKEY
---
<div id="charcha" data-api={api} data-thread={thread} data-turnstile-sitekey={sitekey}></div>
<script is:inline define:vars={{ api }}>
const s = document.createElement('script')
s.src = `${api}/embed.js`
s.defer = true
document.head.append(s)
</script>

Put the address in .env at your project root:

PUBLIC_CHARCHA_API=https://your-worker.workers.dev

Then drop it into your post layout wherever the comments belong:

---
import Comments from '../components/Comments.astro'
---
<article><slot /></article>
<Comments />

The PUBLIC_ prefix is Astro’s convention for values that reach the browser. Both of these do, and both are meant to. Your Worker’s address sits in a script tag either way, and a Turnstile sitekey is public by design.

There is no Charcha package for Astro to install yet. If one arrives it will be this component with types attached, so the shape above is worth writing now rather than waiting.

A partial at layouts/partials/comments.html:

{{ with site.Params.charchaApi }}
<div id="charcha" data-api="{{ . }}"
{{ with site.Params.turnstileSitekey }}data-turnstile-sitekey="{{ . }}"{{ end }}></div>
<script src="{{ . }}/embed.js" defer></script>
{{ end }}

Set the address in hugo.toml:

[params]
charchaApi = "https://your-worker.workers.dev"

Then call {{ partial "comments.html" . }} from layouts/_default/single.html. Wrapping the whole thing in with means a build with no address configured renders nothing at all, rather than a widget pointed at nowhere.

An include at _includes/comments.njk:

<div id="charcha" data-api="{{ charchaApi }}"></div>
<script src="{{ charchaApi }}/embed.js" defer></script>

Put charchaApi in _data/site.js or your global data, then {% include "comments.njk" %} in the post layout.

_includes/comments.html:

<div id="charcha" data-api="{{ site.charcha_api }}"></div>
<script src="{{ site.charcha_api }}/embed.js" defer></script>

With charcha_api: https://your-worker.workers.dev in _config.yml, and {% include comments.html %} in _layouts/post.html.

The widget is not a React component and does not want to be re-rendered. Mount it once and leave it alone.

'use client'
import { useEffect, useRef } from 'react'
const API = process.env.NEXT_PUBLIC_CHARCHA_API
export function Comments({ thread }) {
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) return
mounted.current = true
const script = document.createElement('script')
script.src = `${API}/embed.js`
script.defer = true
document.head.append(script)
}, [])
return <div id="charcha" data-api={API} data-thread={thread} />
}

That mounted ref is doing real work. React runs effects twice in development, and without the guard you get two copies of the script and a confusing afternoon.

The two lines, in your template, before </body>. There is nothing else.

Turnstile is Cloudflare’s bot check. Optional, off until you configure it, free, and invisible to most readers.

It has two keys and they are not interchangeable. Mixing them up is the most common way to break a Charcha install, so it is worth going slowly here.

Key Where it goes Public?
Sitekey data-turnstile-sitekey on the mount element, in your page Yes. It appears in the HTML of every site that uses Turnstile
Secret key On the Worker, as TURNSTILE_SECRET_KEY No. This one never goes in your site

Open the Cloudflare dashboard, go to Turnstile, add a widget, and point it at your site’s hostname. Both keys are on the next screen.

In the Cloudflare dashboard, open Compute (Workers), click your Charcha Worker, then Settings, then Variables and Secrets, then Add. Set the type to Secret, name it TURNSTILE_SECRET_KEY, paste the value and click Deploy.

Setting a secret has that in full, plus the one-line terminal version if you have a checkout and would rather type. It takes effect on the next request either way.

For Astro, the component above already reads it:

PUBLIC_TURNSTILE_SITEKEY=0x4AAAAAAA...

Everywhere else, put it on the mount element:

<div id="charcha" data-turnstile-sitekey="0x4AAAAAAA..."></div>

They are two halves of one switch, and half a switch behaves worse than no switch at all.

Set only the secret, and the Worker starts waiting for a token that nothing on your page produces. Those comments are held for review rather than refused, so what you actually see is a moderation queue filling up with comments that look completely ordinary, and no error anywhere to explain it. After one real token has verified on your deployment, a comment arriving without one is refused outright instead.

Set only the sitekey, and the widget renders, the reader solves it, and nothing checks the answer.

Your dashboard’s Setup tab reports which half the Worker can see. It cannot read your pages, so the sitekey half is yours to confirm.

Charcha stores nothing there, with Turnstile on or off. No cookie, no localStorage, no sessionStorage.

Turnstile itself is Cloudflare’s script running in an iframe on challenges.cloudflare.com, and what happens inside that frame is covered by the Turnstile Privacy Addendum. Worth reading before you switch it on, because switching it on is you adding a third party to your pages.

One setting deserves naming, because it would land on your domain rather than Cloudflare’s. Turnstile’s pre-clearance issues a cf_clearance cookie. Every widget has it off by default, and turning it on is something you would do yourself in the Cloudflare dashboard. Charcha’s no-cookies promise cannot cover a cookie you asked Cloudflare to set. Leave it off, or disclose it.

Your deployment has to know your site is allowed to talk to it. A fresh one trusts only its own address, so until you add yours, the browser refuses every response and the widget reports that it could not load the comments. Getting started has that step, and Theming covers the remaining attributes.