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.
---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_APIconst 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.devThen 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.
Eleventy
Section titled “Eleventy”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.
Jekyll
Section titled “Jekyll”_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.
Next.js, and other React frameworks
Section titled “Next.js, and other React frameworks”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.
Plain HTML
Section titled “Plain HTML”The two lines, in your template, before </body>. There is nothing else.
Turnstile, and where each key goes
Section titled “Turnstile, and where each key goes”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 |
Getting the pair
Section titled “Getting the pair”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.
The secret key goes on the Worker
Section titled “The secret key goes on the Worker”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.
The sitekey goes in your page
Section titled “The sitekey goes in your page”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>Set both or neither
Section titled “Set both or neither”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.
What this puts in your reader’s browser
Section titled “What this puts in your reader’s browser”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.
Before any of this works
Section titled “Before any of this works”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.