Integrations
The widget is framework-agnostic. Every integration is a thin wrapper around the same runtime — no duplicated security logic.
Fast Cloudflare installer
StaticLayer ships a browser-based installer that deploys everything in your Cloudflare account through the official OAuth flow: sign in, pick your account, and it creates the Worker, the D1 database and the three secrets for you. The access token is revoked as soon as the deployment finishes and is never stored.
Start it locally and follow the flow:
npm run dev:installer # → http://localhost:8788
The CLI (npx staticlayer init) is the token-based alternative. Both end with the same Worker + D1 + secrets state. See Universal installation.
Adding comments to specific pages
Every page that includes the widget gets its own thread, keyed by data-article-id. You do not need to edit the snippet per page: when data-article-id is omitted it defaults to window.location.pathname, so the same snippet can live once in your layout/template and every page automatically gets its own thread. Only set data-article-id explicitly when you want a stable key (client-side routing, translated sites, URL migrations).
<!-- one identical snippet in your layout — works on EVERY page -->
<div data-staticlayer
data-api="https://comments.example.com"></div>
<script src="https://comments.example.com/widget.js" defer></script>
<!-- only if you need a stable key, set it explicitly per page: -->
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/hello-world"></div>
Pages without the snippet have no comments; pages with it each get a separate, independently moderated thread.
Connecting sites
There is no per-site registration. A Worker simply serves any page that embeds the widget and points at it. You choose which sites are allowed in two ways:
- Who embeds it: only pages that include the snippet will load your Worker's widget and API. Nothing is served on pages that don't.
- Who may call it cross-origin: add each site origin to
ALLOWED_ORIGINS(CORS). Origins not listed get no cross-origin access — the Worker stays fail-closed.
One Worker can serve many sites and many pages: every data-article-id is its own thread, and the admin console groups them per page. If you prefer hard isolation between sites, deploy a dedicated Worker per site — the installer/CLI make that cheap.
The data-host-context attribute (default location.hostname) is used inside the signed challenge; set it explicitly if the widget runs on a domain different from the page host.
Managing comments
Open https://comments.example.com/admin.html and sign in with ADMIN_SECRET. The admin console shows a single moderation queue across all articles — approve or delete pending comments, with a list of recently published ones. Approved comments appear on their page immediately. Try it in the interactive demo.
Reactions (optional)
Add an anonymous, PoW-protected reaction bar to any page by listing the reactions you want. Three modes — comments and reactions are fully separable:
<!-- 1. comments only (no reactions) -->
<div data-staticlayer data-api="https://comments.example.com"></div>
<!-- 2. comments + reactions together -->
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/hello-world"
data-reactions="👍,❤️,🎉"></div>
<!-- 3. reactions ONLY (standalone, no comment UI) -->
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/hello-world"
data-reactions="👍,❤️,🎉"
data-reactions-only></div>
<script src="https://comments.example.com/widget.js" defer></script>
Because each host element is independent, you can place reactions under the headline and comments at the end of the article on the same page: two elements, one widget.js.
Reactions position (v1.5): by default the whole reactions bar sits under the comment form. Set data-reactions-position="top" to place it above the comments instead — the bar is always kept together, never interleaved with the empty state or the form. Programmatic equivalent: StaticLayer.mount(el, { …, reactionsPosition: 'top' }).
Every click solves a real Proof-of-Work at the difficulty the server issues for that article. Abuse resistance comes from cost, not identity: each reaction is a single-use challenge (atomic anti-replay), reactions are rate-limited per article, a minimum interval is enforced, and difficulty escalates as an article accumulates reactions (base → ceiling, +1 every REACTION_ESCALATION_VOTES).
Privacy: a reaction is an anonymous event (article, reaction, timestamp). No user id, no IP, no cookie, no fingerprint. Honest limit: repeated votes by the same person are not detectable by design — the guarantee is cost, not identity.
Customization (all optional vars, in your worker config):
REACTION_OPTIONS— comma-separated allowed reactions (default👍,❤️,🎉; empty disables reactions)REACTION_DIFFICULTY_BASE— difficulty for the first reactions (default 16)REACTION_DIFFICULTY_CEILING— hard cap (default 20)REACTION_ESCALATION_VOTES— votes per +1 difficulty step (default 20)REACTION_MIN_INTERVAL_SECONDS— minimum seconds between accepted reactions on the same article (default 3)
Programmatic equivalent: StaticLayer.mount(el, { …, reactions: ['👍', '❤️'] }).
Polls (optional)
StrawPoll-style polls, created in the admin (Polls tab) and embedded anywhere with a dedicated host element — fully independent of comments/reactions:
<div data-staticlayer
data-api="https://comments.example.com"
data-poll-id="<poll id from the admin>"></div>
<script src="https://comments.example.com/widget.js" defer></script>
Votes are anonymous (no IP, no cookie) and protected by the same Proof-of-Work + atomic anti-replay as comments. The optional one vote per browser guard issues an anonymous token that lives only in the visitor's browser and stores only a hash on your D1 — a returning visitor cannot vote twice, and no personal data is collected. Honest limit: clearing browser storage resets the guard, as with every anonymous poll.
Multi-select (v1.5): toggle Multi-select in the admin and visitors can pick several options, then vote once for the whole set — a single Proof-of-Work covers every selected option (+1 each). Results are shown as a ranking (highest first, #1/#2 badges), the leader is highlighted, and the heading shows the total votes (e.g. “9 votes”).
Change your votes: on multi-select polls with the one-vote-per-browser guard, a returning visitor sees “Change your votes” — revoking their own previous (anonymous) votes so they can vote again. Nothing is stored about who they are.
Global poll: leave the article path empty when creating the poll and it appears on any page where you embed its id. Optional display attributes: data-poll-style="bars|percent|counts|minimal" and data-poll-results="after|always".
Programmatic equivalent: StaticLayer.mount(el, { …, pollId: '<id>' }).
Nested replies
Comments support replies up to 3 levels: click Reply on any comment to open an inline form with the same Proof-of-Work + moderation pipeline. A reply becomes public only when it and its parent are approved; if a parent is deleted, its replies stay visible with a "comment removed" placeholder. The admin queue shows a ↳ reply to badge on replies.
Owner replies: answer from the admin console with Reply — the reply is approved instantly and marked with an Author badge on the widget. The owner nickname is configurable (Settings → Owner nickname).
Comment engagement (v1.6)
- Likes: visitors like a comment with one anonymous tap (PoW-protected). A per-browser guard stores only a hash of an anonymous token, so a browser can like each comment once.
- Thread sort:
data-comments-sort="newest|oldest|best"(default newest) — a small selector appears in the widget heading once there is more than one comment. Pinned comments always stay on top. - Pin: from the admin, press Pin on any comment card to keep a comment at the top of its thread.
- Report: visitors can flag a comment with a signed proof-of-work — the admin sees ⚠ N reports in moderation. A flag stores only {comment, time}: zero data.
- Relative timestamps ("3 h ago") and "Read more" for long comments.
- GDPR export: admin → Settings → Export your data — comments as CSV or the full dataset as JSON. No IPs, no tracking data.
Framework templates
Drop-in components that render the widget with the same attributes — see integrations/ in the repository: Astro (Comment.astro), React/Next (Comment.tsx), Vue/Nuxt (Comment.vue), Hugo (partial) and Jekyll (include). Every option maps 1:1 to the widget's data-* attributes.
Where to place it on your page
The widget renders exactly where you put the host element in your HTML — the script tag can stay once in your layout. Two jobs, two placements:
- Comments (discussion): immediately after the article content, before the footer — in your content column (the widget centers itself, max-width 640px). Readers finish, then engage. Avoid sidebars and above-the-fold placement on content pages.
- Reactions (quick feedback): one-tap and unmoderated, so they work higher — right under the headline for maximum visibility, or just above the comments ("react, then discuss").
Comments and reactions are independent elements: use one, both in the same place, or each in its own spot (e.g. reactions under the title, comments at the bottom — see Reactions). Only pages with a snippet get a widget at all.
Vanilla HTML
<div data-staticlayer
data-api="https://comments.example.com"
data-article-id="/blog/my-post"></div>
<script src="https://comments.example.com/widget.js" defer></script>
GitHub Pages
Default domain — https://username.github.io/project/
The Worker is a separate origin. Point the widget at it and configure CORS for your site origin:
<div data-staticlayer
data-api="https://your-worker.workers.dev"
data-article-id="/project/post-slug"></div>
<script src="https://your-worker.workers.dev/widget.js" defer></script>
Limitation: the Worker is a separate origin, so cross-origin requests require the allowed-origin (CORS) configuration on the Worker. See Security.
Custom domain — https://blog.example.com
Recommended: run the Worker on comments.example.com (a Cloudflare custom domain where the Worker is the origin), and serve the site from GitHub Pages. DNS can remain outside Cloudflare; only the Worker needs a reachable hostname.
Cloudflare Pages
If DNS and hosting are on Cloudflare, the same-account setup is simplest: the site, the Worker and D1 live in one account. Use a Custom Domain when the Worker is the origin for comments.yourdomain.com, or a Route when the Worker runs in front of an existing origin. Verify against the current official Cloudflare documentation before configuring.
Astro
Add the snippet to your layout (e.g. src/layouts/BaseLayout.astro) before </body>. No component library required.
Hugo
Add the snippet to layouts/_default/single.html or a partial included in your single-page template.
Jekyll
Add the snippet to _layouts/post.html (or a comment include).
Next.js (static export)
StaticLayer is intended for static output; no server-side runtime is required. Put the snippet in your post layout. Set output: 'export' in next.config and deploy the static build.
Generic static hosting
Any static site that can embed JavaScript can use StaticLayer with the appropriate API origin and CORS configuration. This includes Netlify static, Vercel static export, S3/CloudFront static sites and plain web hosting. These are documented as compatible by design; they are not separately tested official integrations.