Add a website
Do this once for each website. You add the website in the console, put Vizoalica on its pages, and check that page views arrive. Your backend does not change, and nothing else needs to run on your computer once the website is live.
There are two ways to install:
| Way | Use it when |
|---|---|
| One script tag | Almost always. Any host works: GitHub Pages, Netlify, WordPress, plain HTML files. Nothing to run on your site. |
| Signed tokens (optional) | Your site can run a small server function (for example on Cloudflare Pages) and you want fake events to be harder to send. |
One script tag
Open the console, choose Websites, then Add website. Enter the address people open (for example
example.com) and leave Require a signed token off. Save.The console opens the website's Install page with one line to copy:
html<script defer src="https://prod-vizoalica.<you>.workers.dev/vizoalica.js" data-source="<website key>" data-token-url="none" ></script>Paste it before
</body>on every page, or once in your shared layout or theme.Publish your site, then choose I've deployed, check now. The console opens your home page, looks for the tag, and shows whether page views have arrived.
The tag holds only public values. Your backend serves the script, so there is no file to copy and nothing to update on your site when you update Vizoalica. The backend accepts events only from the addresses you entered for the website; everything else is refused.
How visitors are counted. Vizoalica stores nothing in your visitors' browsers: no cookies and no local storage. The backend counts unique visitors with an identifier made from a random value that changes every day, the visitor's IP address, and their browser's user agent. The IP address and user agent are never stored, and each day's random value is deleted after the day ends, so nobody can link a visitor to that identifier or to another day. The same person on two days, or on two of your websites, counts as two visitors. See Privacy defaults.
Limits to know. Anyone can send events that claim to come from your site, because a browser's origin header can be faked outside a browser. Rate limits and per-website quotas cap how much such traffic can cost you. If that matters for your site, use signed tokens.
A content security policy, if your site has one, must allow your backend's address in script-src and connect-src.
Signed tokens (optional)
With Require a signed token on, every batch of events needs a short-lived token that your site's own server signs with VIZOALICA_TOKEN_SECRET. That secret is shared by every website on the backend, so treat it like a password: a leak from one website lets someone sign tokens for all of them (see "Suspected signing-secret exposure" in the backend guide). Keep rate limits on, and keep your site's own login for private pages: a token proves the request came through your server, not who the visitor is.
You need:
- A working backend and console.
- The backend's
VIZOALICA_TOKEN_SECRET. It is not shown when the backend is created. The first time a website needs it, runvizoalica rotate <backend> token: it makes a new one and shows it once. Save it in a password manager. Reuse that value for every later signed-token website. - Control of the website's build and deployment.
- Node.js 22 or newer and pnpm 9, for the manual path.
- For the GitHub Actions path: the website's GitHub repository, and a Cloudflare API token scoped to Cloudflare Pages: Edit only. For the manual path:
wrangler login, or a working Git integration.
The token Function runs with your website, so your computer can be off while the site collects events.
1. Create the website in the console
Open the console, select Websites, then Add website. Enter the address with https:// and no trailing slash, turn on Require a signed token, and save. The Install page opens. The website's own page lists its project ID, website ID, and website key, each with a copy button; all three are public values, and they are different from each other.
2. Choose a deployment path
| Path | Use it when |
|---|---|
| A. GitHub Actions | The website's source is in its own GitHub repository. Recommended. |
| B. Manual | Direct Upload, an existing Git-connected Pages project, or your own endpoint. |
Both paths end at Verify website activation.
Path A: GitHub Actions (recommended)
A push to the website's repository deploys it automatically, with no per-deploy manual steps and no analytics values ever committed to the website's source.
- Open the website's Install page in the console (from the website's page, choose Install) and choose GitHub → Cloudflare Pages, the recommended path (it uses dynamic configuration).
- Copy the generated guidance: the page shows the exact GitHub repository variables and secrets to add, a ready-to-paste starter workflow file, and the equivalent
ghCLI commands. Add the variables/secrets to the website's repository (GitHub → Settings → Secrets and variables → Actions), and the two account-specific values (CF_ACCOUNT_ID,CF_PAGES_PROJECT) plus a Cloudflare API token scoped to Cloudflare Pages: Edit only (create it without any Client IP Address Filtering restriction — a CI runner has no fixed IP). - Add the workflow file shown on the page to
.github/workflows/in the website's repo, and push. The workflow deploys the site, vendors the Vizoalica configuration and token-issuing Functions, and wires the analytics configuration into the Cloudflare Pages environment — all from repository variables/secrets, never from committed source. - Verify with Verify website activation against the deployed URL, and check the website's page in the console shows Website reachable.
See the deploy workflow contract for the full technical contract, including the caveat that a public website repository cannot call a reusable workflow hosted in a private repository — vendor the workflow's steps directly into the website's own repo instead in that case (see the contract for the exact reason).
Path B: manual (Direct Upload or Git-connected Pages)
For a website not using GitHub Actions, or a snippet with your own token endpoint. Each step is run from your Vizoalica checkout unless it says otherwise.
B1. Prepare the website project
From your Vizoalica checkout, choose an unused Pages project name and a private working copy for the example. Replace the two paths/names below. The name determines your pages.dev origin.
export VIZOALICA_SITE_DIR="$HOME/vizoalica-demo"
export VIZOALICA_PAGES_PROJECT="YOUR_PAGES_PROJECT"
mkdir -p "$VIZOALICA_SITE_DIR"
cp -R examples/cloudflare-pages/. "$VIZOALICA_SITE_DIR/"
cp "$VIZOALICA_SITE_DIR/wrangler.example.toml" "$VIZOALICA_SITE_DIR/wrangler.toml"
pnpm exec wrangler pages project create "$VIZOALICA_PAGES_PROJECT" --production-branch mainFor an existing Pages project, skip creation and use its actual name and production branch. Do not overwrite an existing site's files; merge the Function and SDK into its own structure as explained below.
Check: the Pages project exists in the intended Cloudflare account. Record the exact origin shown in the dashboard, normally https://YOUR_PAGES_PROJECT.pages.dev.
The console's install check
On a website's install page, "I've deployed, check now" looks at each allowed address for /vizoalica.js, then asks /vizoalica/ingest-token for a token (with the site's own Origin), and answers with the first thing that is wrong: file missing, token endpoint missing or erroring, the origin not in the endpoint's list, or a redirect to an address that is not allowed. It never follows a redirect, never reads a response body, and discards the token, which expires in five minutes. Then it reads the last day's page views. Add the address a site redirects to (for example www) to the allowed origins and to the endpoint's list, or list only the address that serves the site.
B2. Fill in the public configuration
Defaults (0.7.3).
VIZOALICA_SDK_SRC(/vizoalica.js),VIZOALICA_TOKEN_URL(/vizoalica/ingest-token) andVIZOALICA_CONSENT(unknown) follow a convention, so the deploy workflow no longer asks for them. It also accepts one repository variable,VIZOALICA_SITE, holdingendpoint,sourceKey,projectId,sourceIdandoriginsas JSON, in place of the five values that vary per website; a separate variable still wins over the bundled one.site-directorydefaults to the repository root. Changing a website's allowed origins in the console means updating the origin list here (or inVIZOALICA_SITE) to match, then deploying. The manualwrangler.tomlbelow still uses the separate fields.
Edit the working copy's wrangler.toml:
| Field | Copy from |
|---|---|
name | Pages project name |
VIZOALICA_SDK_SRC | Public SDK path, normally /vizoalica.js |
VIZOALICA_INGEST_ENDPOINT | Console's dynamic public configuration |
VIZOALICA_PUBLIC_SOURCE_KEY | Console's website key |
VIZOALICA_PROJECT_ID | Console's Project ID |
VIZOALICA_TOKEN_URL | Same-origin /vizoalica/ingest-token |
VIZOALICA_CONSENT | Recorded state after the host grants analytics consent |
VIZOALICA_SOURCE_ID | Console's website ID; server-side token scope |
VIZOALICA_SITE_ORIGINS | Comma-separated exact website origin(s), no trailing slash; list every hostname (e.g. apex + www) that serves the site |
Edit public/index.html, replacing its three REPLACE_… values with your Worker hostname, website key and project ID. The Worker hostname excludes https://; keep the full endpoint ending in /v1/events:batch. The demo serves its SDK at /vizoalica.js.
Check: no REPLACE_… values remain in wrangler.toml or public/index.html. Do not put a secret in either file. The website ID is not the website key.
B3. Build and copy the browser SDK
Run from the Vizoalica checkout:
pnpm browser-sdk:build
cp packages/browser-sdk/dist/vizoalica.js "$VIZOALICA_SITE_DIR/public/vizoalica.js"
cp packages/browser-sdk/dist/vizoalica-loader.js "$VIZOALICA_SITE_DIR/public/vizoalica-loader.js"Check: public/vizoalica.js exists in the website copy. This is a standalone IIFE bundle; you do not need a JavaScript framework, CDN account, or an extra server. pnpm build builds the workspace packages; pnpm browser-sdk:build is the explicit command for a script-tag bundle. Rebuild and recopy it when upgrading Vizoalica. Your website hosts its own copy, so an update reaches visitors only when you recopy and deploy it. The 0.6 SDK file reports each page of a single-page site, groups identifiers in paths, and records clicks on buttons and links as actions (see the Browser SDK); upgrade the backend first, then the SDK file.
Your website now has this structure:
vizoalica-demo/
├── wrangler.toml
├── functions/
│ └── vizoalica/
│ ├── config.json.ts
│ └── ingest-token.ts
└── public/
├── index.html
├── vizoalica.js
├── vizoalica-loader.js
└── _routes.jsonThe public configuration Function maps six plaintext variables to the portable version 1 JSON contract with no-store/nosniff headers and no partial fallback. The token Function uses server configuration for project/website/origin, signs HS256 tokens with Web Crypto, sets a five-minute lifetime and a 25-event token limit, and returns text/plain with Cache-Control: no-store. Request parameters cannot select another project or origin. It rejects missing configuration, foreign provenance and requests on unconfigured preview domains.
The shared signing secret stays on trusted servers. Use the site's existing session authentication as an additional requirement if the site is private.
Review sequence for dynamic Cloudflare configuration
Keep this order and stop before deployment until the account, Pages project, environment, production branch, site directory, output directory, public-variable block, Function, loader, and route diff are approved:
pnpm exec wrangler whoami
pnpm exec wrangler pages project list --json
git diff -- wrangler.toml functions/vizoalica/config.json.ts public/vizoalica-loader.js public/_routes.json
pnpm exec wrangler pages dev public --cwd "$VIZOALICA_SITE_DIR"
# After review, use the site's actual Git-connected flow or the Direct Upload command below.
pnpm exec wrangler pages deploy public --cwd "$VIZOALICA_SITE_DIR" --project-name "$VIZOALICA_PAGES_PROJECT" --branch main
pnpm exec wrangler pages deployment list --project-name "$VIZOALICA_PAGES_PROJECT"
pnpm website:verify -- https://YOUR_PAGES_PROJECT.pages.dev YOUR_PROJECT_ID YOUR_WEBSITE_ID --mode dynamicB4. Save the same signing secret on Pages
Paste the same VIZOALICA_TOKEN_SECRET value used by the ingestion Worker into the hidden prompt:
pnpm exec wrangler pages secret put VIZOALICA_TOKEN_SECRET --project-name "$VIZOALICA_PAGES_PROJECT" --cwd "$VIZOALICA_SITE_DIR"
pnpm exec wrangler pages secret list --project-name "$VIZOALICA_PAGES_PROJECT" --cwd "$VIZOALICA_SITE_DIR"Check: the name exists on both the Worker and Pages project. Secret listing cannot prove their values match; the browser's accepted event in Verify website activation does. Do not create a second signing value for Pages. The admin key and Cloudflare deployment token never go on this website. For an existing project with separate preview/production settings, confirm the secret and variables in the Production environment in the dashboard, then redeploy for changes to take effect.
B5. Deploy the website and Function together
For this Direct Upload example, run from the Vizoalica checkout:
pnpm exec wrangler pages deploy public --cwd "$VIZOALICA_SITE_DIR" --project-name "$VIZOALICA_PAGES_PROJECT" --branch mainCheck: the deployment output says the Functions were compiled/uploaded, as well as the assets. Verify the stable production origin from B1. The unique preview URL printed for a deployment is not automatically an allowed analytics origin.
--cwd is essential: Wrangler must start in the website project to discover its functions/ directory. Passing an absolute asset directory from another working directory can upload HTML without the Function. functions/ belongs beside public/, not inside it. These locations and flags follow Cloudflare's Function setup and Pages command reference.
Existing website: Git-connected or Direct Upload?
In Cloudflare Workers & Pages → your Pages project, inspect the connected repository and build settings, production branch, and latest deployment commit. A Git-connected project has a repository connection; a Direct Upload project does not gain one merely because your files are in Git.
| Project mode | Deploy changes |
|---|---|
| Git-connected, automatic builds enabled | Commit the site changes and git push origin main from the site repository (substitute its actual production branch). Confirm a new Cloudflare deployment for that commit. |
| Direct Upload, or intentional manual upload | Build locally, then use the wrangler pages deploy … --cwd … --project-name … --branch … command above. A Git push alone does nothing. |
For a Git-connected site, ensure its build emits vizoalica.js into the configured output directory. If using the prebuilt bundle, commit it as a website asset so the remote build actually has it. The Function belongs in the configured project root and the output directory must contain _routes.json. On an existing site, merge the example route into its routing configuration; do not replace routes for other Functions. Likewise merge variables into its existing Wrangler configuration rather than copying over its settings. If its production branch is not main, use that branch in every command here.
See Cloudflare's Git integration and Direct Upload guides. Dashboard drag-and-drop does not compile a functions/ directory; use Wrangler or a Git build.
Verify website activation
From the Vizoalica checkout, substitute your stable origin, project ID and website ID:
pnpm website:verify -- https://YOUR_PAGES_PROJECT.pages.dev YOUR_PROJECT_ID YOUR_WEBSITE_ID
pnpm website:verify -- https://YOUR_PAGES_PROJECT.pages.dev YOUR_PROJECT_ID YOUR_WEBSITE_ID --mode dynamicCheck: the command reports that website content and token claims passed. It checks:
- Static mode checks
/vizoalica.js; dynamic mode checks/vizoalica-loader.jsand the complete, project-scoped/vizoalica/config.jsonresponse. /vizoalica/ingest-token:text/plain,no-store, three JWT parts, HS256 header, expected project/website/origin, audience/scope, expiry and five-minute lifetime.
It sends a same-origin referrer like a browser GET. It never prints the token. It checks token structure and claims, not the signature: only the Worker's accepted event proves that the deployed signing secrets agree. An HTTP 200 alone is insufficient; Pages may return fallback HTML for either missing path.
Now open the website and grant analytics consent (the demo page has an Allow analytics button). In the browser Network panel, confirm the batch request to the Worker returns 202, then refresh the local console's 24h view. In a fresh test website, one visit produces one page view and one privacy-safe unique user. See privacy operations for the data boundary. The demo makes the choice per visit; refresh to choose again. Finally, block the Worker request in the browser and reload: the website's primary content and controls must remain usable.
Add the integration to your own pages
Choose exactly one path on the website's Install page after hosting the SDK and token Function. Paste a snippet (the static option) is the existing complete website-specific snippet. Dynamic configuration keeps the generic loader unchanged while the hosting adapter supplies the six public values. It uses the first configured website origin for src and your configured Worker for data-endpoint. For several allowed origins, hosting at the same /vizoalica.js path lets you use a relative src. Install on every page or shared layout where you want collection. Check the deployed page source; an integration that used to exist in a sample repository may have been removed.
data-consent records the consent state on each event; it is not a consent banner. To send nothing for a visitor who declines, do not load the script. Whether your site needs a banner for analytics depends on the rules that apply to it; Vizoalica itself stores nothing in the browser.
For CSP-restricted sites, allow the SDK's host in script-src and the Worker origin in connect-src; retain your existing policy. Keep same-origin referrers enabled for the token GET. If your site deliberately uses Referrer-Policy: no-referrer, adapt its authenticated backend rather than weakening this example's provenance check.
For a non-Cloudflare host, serve the same /vizoalica-loader.js and version 1 JSON document from its public configuration mechanism. Keep field names, validation, no-store behavior, consent gate, and failure isolation identical. When switching modes, remove the other script path, review the deployed output, and verify again before resuming collection.
Rotate or remove
For a website change, rebuild and copy the SDK, deploy the Function with the website, and rerun all website verification. Updating the backend itself is a separate operation; see Update an existing backend.
For signing-key rotation, pause collection across every connected website, replace the secret on the Worker and every trusted token issuer, redeploy, verify a new token and accepted event, then resume. This single-key example has no overlapping-key rotation; old tokens fail after the Worker key changes. Keep preview websites and secrets separate from production.
To stop one website, disable it in the console and remove the SDK load from the site's shared layout. Confirm new events are rejected while the website remains usable. Delete is terminal and permanent: new events are rejected at once, and the daily Cron run then removes the website's raw event batches in R2 and every D1 row for it, including audit entries. Deleting a project does the same for the project and all its websites. It does not remove the backend, another website, or your console.
To purge immediately instead of waiting for the Cron run, use pnpm vizoalica purge-deleted to list what would go, then pnpm vizoalica purge-deleted --apply. Each run is bounded and resumes on the next, so a large purge can take more than one run. A purge is audited without naming what it removed.