Detect Encited visits
Encited loads your pages in a real browser twice over: once to capture the snapshot crawlers receive, and once when the SEO Spider audits your site. Your JavaScript runs both times, so analytics fire and bot protection engages, exactly as they would for a person.
Which signal you use depends on where you need to act.
| Where you act | Signal | Covers |
|---|---|---|
| In the page, for analytics and pixels | window.__ENCITED__ |
Renders and audits |
| At your firewall or CDN | x-encited-token request header |
Renders and audits, once you set a token |
| In server logs | Encited-SEOAuditBot user agent |
Audits only |
In the page: the marker#
Every Encited visit that runs JavaScript sets a global before any of your scripts:
window.__ENCITED__ = { visit: "render" }; // or "audit"
visit |
What it is |
|---|---|
"render" |
Capturing an HTML snapshot for crawlers |
"audit" |
SEO Spider crawling your site for a report |
It's injected before the document is parsed, so a synchronous check at the top of your <head> never races your tracking snippets.
if (!window.__ENCITED__) {
// load analytics, pixels, chat widgets, session recording
}
To exclude only one kind of visit:
if (window.__ENCITED__?.visit !== "audit") {
// runs for humans and renders, skipped during audits
}
Render visits also set the legacy window.__TO_HTML = true flag. It still works, but prefer window.__ENCITED__, which covers audits too.
Google Analytics#
<script>
if (!window.__ENCITED__) {
var s = document.createElement("script");
s.async = true;
s.src = "https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX";
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("js", new Date());
gtag("config", "G-XXXXXXX");
}
</script>
Google Tag Manager#
Guard the container snippet so no tags load at all:
<script>
if (!window.__ENCITED__) {
(function (w, d, s, l, i) {
w[l] = w[l] || [];
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
var f = d.getElementsByTagName(s)[0],
j = d.createElement(s);
j.async = true;
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + "&l=" + l;
f.parentNode.insertBefore(j, f);
})(window, document, "script", "dataLayer", "GTM-XXXXXXX");
}
</script>
To keep GTM loading and block individual tags instead, add a Custom JavaScript variable:
function () {
return Boolean(window.__ENCITED__);
}
Then add a trigger exception on each tag for when it equals true.
PostHog#
if (!window.__ENCITED__) {
posthog.init("phc_your_key", { api_host: "https://us.i.posthog.com" });
}
Or let it load and opt the visit out:
posthog.init("phc_your_key", {
api_host: "https://us.i.posthog.com",
loaded: function (ph) {
if (window.__ENCITED__) ph.opt_out_capturing();
},
});
Plausible, Mixpanel, Meta Pixel, Hotjar and Clarity all take the same if (!window.__ENCITED__) wrapper.
TypeScript#
declare global {
interface Window {
__ENCITED__?: { visit: "render" | "audit" };
}
}
At your firewall: the render token#
If your site sits behind a firewall, bot protection, or rate limiting, those rules block Encited. Pages fail to render, audits come back full of errors, and crawlers get served whatever your origin returns to a blocked request.
Open Site settings for your domain, find Render token, and press Generate, or paste a value you already use.
Put the value in your firewall rule first, then save your settings. From then on our requests carry:
x-encited-token: 4affb2bce678ddf8b018832d0345aaec
Most firewalls can match a header value exactly:
if http.request.headers["x-encited-token"] == "<your token>"
-> skip bot protection and rate limiting
Rules by provider#
Cloudflare. WAF → Custom rules, with this expression:
http.request.headers["x-encited-token"][0] eq "<your token>"
Action Skip, then tick Super Bot Fight Mode, Rate limiting rules, and any managed rules that run on your pages. Header names are lowercase in Cloudflare expressions.
Bot Fight Mode on the free plan is the exception. It runs ahead of custom rules and can't be skipped, so move to Super Bot Fight Mode or turn it off for the hostname.
Vercel. Firewall → Custom rules. Condition: Request Header, x-encited-token, equals your token. Action Allow, which stops later rules evaluating, so place it above your bot and rate-limit rules.
Fastly. On the Next-Gen WAF, add a request rule matching the header with action Allow. On plain VCL, set a flag and check it wherever you throttle:
if (req.http.x-encited-token == "<your token>") {
set req.http.X-Encited-Allow = "1";
}
AWS WAF. Add this to the web ACL with a lower priority number than Bot Control and any rate-based rule, so it evaluates first. Allow ends evaluation for that request, so nothing after it runs.
{
"Name": "AllowEncited",
"Priority": 0,
"Action": { "Allow": {} },
"Statement": {
"ByteMatchStatement": {
"FieldToMatch": { "SingleHeader": { "Name": "x-encited-token" } },
"PositionalConstraint": "EXACTLY",
"SearchString": "<your token>",
"TextTransformations": [{ "Priority": 0, "Type": "NONE" }]
}
}
}
Through the CLI or JSON API, SearchString has to be base64-encoded.
Netlify. Firewall traffic rules are Enterprise and match on IP, country and user agent rather than arbitrary headers. Do the check in an edge function instead, and let matching requests through before your bot logic runs.
Which requests carry it#
The token goes only to your own hostnames: your domain, its www form, and the project URL set in Site settings. Requests to any other host, such as a CDN or analytics vendor your pages load, never receive it.
It's a request header, so it never appears in the HTML we serve and no search engine sees it.
The project URL test in Site settings and sitemap fetches don't carry it, so those can report your site as unreachable while renders work.
Changing it#
- Press Generate for a new value, or type your own. Nothing is sent yet.
- Add that value to your rule, next to the current one.
- Save your settings. We switch to the new value.
- Wait two hours, then remove the old value from your rule.
A render that started before you saved still carries the old value until it finishes, which is what the wait covers.
To remove it, clear the field and save. New work stops carrying the header, anything already running keeps it for up to two hours, then take the rule out.
In your logs: the audit user agent#
Audit crawls identify themselves:
Encited-SEOAuditBot/1.0 (+https://encited.com/bot)
Older runs may show LvHTML-SEOAuditBot, so match either. Renders can't be filtered this way, by design. Use the marker or the token for those.
Fixes by symptom#
| Symptom | What to check |
|---|---|
| Pageviews inflated after connecting | Analytics isn't guarded. Wrap the loader in if (!window.__ENCITED__) |
| Renders fail or snapshots hold a challenge page | Bot protection is blocking us. Set a render token and add the rule |
| Audits return errors on every page | Same cause. Confirm the rule matches the saved token exactly |
| Rule stopped matching after a rotation | The old value was removed from the rule too early. Both values need to sit in the rule for two hours |
| Site settings says the origin is unreachable, but renders work | The project URL test doesn't carry the token. Expected |
| Conversions firing on ad pixels | Guard the pixel with the marker, not the token. Pixels run in the page |
There is no render user agent to match#
Encited renders load your pages as ordinary Chrome, in a headless chrome browser.
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36
