Repository navigation
Customize UI
The basic challenge and fail pages are very generic and quite honestly ugly. If you need to provide a better UI, you can do so with custom templates.
You can choose to set up a TEMPLATE_PATH to point to wherever you want to
store these templates, or just stick with the default: /var/local/tps/templates.
Copy the *.go.html files from internal/templates to the top of your
template path, adapt them, and you're done:
/var/local/tps/templates/challenge.go.html
/var/local/tps/templates/failed.go.html
Those two replace TPS's built-in pages for every request TPS handles, whatever the host or path. That's all most sites ever need.
If one site behind TPS needs its own look, or one URL deserves its own explanation, add subdirectories. A directory directly under your template path is a hostname, and directories under that match the URL path:
/var/local/tps/templates/challenge.go.html <- everything
/var/local/tps/templates/front.x.edu/challenge.go.html <- one site
/var/local/tps/templates/front.x.edu/collections/bread/challenge.go.html <- one section
TPS picks the most specific template it can find, then falls back: the deepest matching path, then the hostname, then your top-level pair, then the built-in page. You can mix and match — a site-specific challenge page with the shared failure page is fine, since each is looked up on its own.
Note: the hostname directory is matched against the Host header, which
means the public hostname, and it includes the port if the browser used
one. If TPS is listening to front.x.edu and proxying to backend.x.edu, the
directory is front.x.edu, never backend.x.edu. In development against
http://localhost:8080/, it's localhost:8080.
The path directories match whole URL segments, and a match covers everything
below it: collections/bread/ serves /collections/bread and
/collections/bread/search?q=rye alike.
A challenge page has to do three things that have nothing to do with how it looks: load Cloudflare's Turnstile script, present a form that posts the solved token back to the URL the user asked for, and submit that form when the widget succeeds (while pinging the beacon so TPS knows a real browser ran the page).
You don't have to write any of that. Put an empty <challenge-form> element
wherever the widget belongs and TPS fills it in when it loads your template:
<!DOCTYPE html>
<html>
<head>
<title>Just a moment</title>
<link rel="stylesheet" href="https://example.edu/site.css">
</head>
<body>
<h1>Verifying your browser</h1>
<p>Trouble getting through? <a href="/contact">Let us know</a>.</p>
<challenge-form></challenge-form>
</body>
</html>Details worth knowing:
- The Turnstile script is added to the end of your
<head>(or the top of your<body>if you have no<head>). If your template already loadschallenges.cloudflare.com/turnstile/v0/api.jsitself, TPS leaves it alone — loading it twice makes Turnstile render every widget twice. - Attributes on the element are kept, so
<challenge-form class="widget">works, as does stylingchallenge-form { ... }in your CSS. It's an unknown element, so browsers treat it asdisplay: inlineuntil you say otherwise. - Anything you put inside the element stays there, after the injected form.
That's a decent place for a
<noscript>explanation. - Use it once per page. Two placeholders mean two Turnstile widgets and two forms with the same id; TPS logs a warning if it sees that.
- The generated form has the id
tps-challenge-form, and the callback the widget uses is namedtpsChallengeSolved. Avoid those two names in your own markup and scripts.
The core templates in internal/templates use the placeholder too, so they're
a working example of the whole thing.
Hand-written challenge markup still works. If your template has no
<challenge-form> element, TPS doesn't touch it at all — you're on the hook
for the Turnstile script, the form (posting request_id and the
cf-turnstile-response the widget produces to {{.PostAction}}), and the
navigator.sendBeacon('/.tps/beacon') call. Copying the current core template
and stripping the placeholder is the easy way to see what that involves.
If you need to change a template, you must restart TPS in a production environment. Templates will auto-reload on change in dev, but not in production!
In dev (any GIN_MODE other than release) TPS re-reads the file on every
render, so saving the file and refreshing the page is the whole loop. If you
save a template that doesn't compile, TPS logs the error and keeps serving the
last version that did, so a stray {{ doesn't leave you staring at a broken
page while you look for the typo. In production, templates come from the
binary's embedded copy or are read once at startup, and neither is re-read
while TPS runs.