Prerequisites
- An Encited account and API key (Settings → API Keys)
- A domain added + verified in your Encited dashboard
- Cloudflare account with Workers enabled
- Standard Routes setup only: your domain's DNS managed by that Cloudflare account, with the A/CNAME record proxied (orange cloud), since Worker routes only run on traffic that passes through Cloudflare's proxy. The Custom Domain flow (Lovable-style hosting) doesn't need this; Cloudflare creates the DNS record for you.
Standard setup
For custom managed servers, WordPress, Shopify, BigCommerce and others.
Create a new Worker
Open Cloudflare dashboard → choose a "Hello World" Worker → Deploy → Edit Code
Paste the snippet below into the Worker editor
// encited.js (Cloudflare Worker)export default {async fetch(req, env) {// Only handle public GET navigationsif (req.method !== 'GET') return fetch(req);// Treat missing/empty Accept and bare '*/*' as HTML so crawler tests// (curl without -H, default fetch) still route through prerender.// Asset requests from browsers send specific Accept (e.g. 'text/css,*/*;q=0.1')// so they won't match.const accept = (req.headers.get('accept') || '').trim();const isHtmlRequest = !accept || accept === '*/*' || accept.includes('text/html');if (!isHtmlRequest) return fetch(req);const headers = new Headers();headers.set('x-lovablehtml-api-key', env.LOVABLEHTML_API_KEY);headers.set('accept', 'text/html');const forward = ['accept-language','sec-fetch-mode','sec-fetch-site','sec-fetch-dest','sec-fetch-user','upgrade-insecure-requests','referer','user-agent',];for (const name of forward) {const v = req.headers.get(name);if (v) headers.set(name, v);}try {const r = await fetch('https://encited.com/api/prerender/render?url=' + encodeURIComponent(req.url),{ headers, redirect: 'manual' },);// 301 = configured redirect rule matched - forward to clientif (r.status === 301) {const loc = r.headers.get('location');if (loc) {return new Response(null, {status: 301,headers: { location: loc, 'cache-control': 'no-store' },});}}// 304 = not pre-rendered, pass through to originif (r.status === 304) {return fetch(req);}if (r.status === 200 && (r.headers.get('content-type') || '').includes('text/html')) {const responseHeaders = new Headers(r.headers);for (const name of ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'keep-alive']) {responseHeaders.delete(name);}responseHeaders.set('content-type', 'text/html; charset=utf-8');return new Response(r.body, { status: 200, headers: responseHeaders });}} catch {// Prerender unreachable → fall through so visitors still get the site}return fetch(req);},};
Add the API key secret
Under Variables and Secrets, add a secret named LOVABLEHTML_API_KEY or run: wrangler secret put LOVABLEHTML_API_KEY
Confirm your DNS record is proxied
Routes need your domain's DNS to be managed in the same Cloudflare account. In DNS → Records, the A or CNAME record for yourdomain.com must show an orange cloud (Proxied), not gray (DNS only). The Worker only runs on traffic that passes through Cloudflare's proxy.
Add a route to your Worker
Go to your Worker → Settings → Domains & Routes → Add Route → enter yourdomain.com/*. If your site is reachable on both yourdomain.com and www.yourdomain.com, enter *yourdomain.com/* instead so both are covered.
Hosting on a subdomain? Use blog.yourdomain.com/* for just that subdomain, or *.yourdomain.com/* to cover every subdomain.
Set Failure mode
Set to Fail open (proceed) and save. If the Worker ever errors, requests continue straight to your origin instead of failing.
Let crawlers through Cloudflare's bot settings
If you use Bot Fight Mode or AI crawler blocking (Security → Bots), allow the crawlers you want pre-rendered. Those protections run before your Worker, so a blocked crawler never reaches it.
Deploy the Worker
It can take a couple of minutes to start working.
Setup for BigCommerce
For BigCommerce stores on a custom domain.
BigCommerce manages your hostname through its own Cloudflare account (Cloudflare for SaaS). With BigCommerce's default DNS instructions, Cloudflare hands requests directly to BigCommerce, so they never reach your Worker even when a route is attached. The fix below works on any Cloudflare plan.
Point your DNS at BigCommerce with a proxied CNAME
In your Cloudflare zone, the store hostname (www or a subdomain) must be a CNAME to shops.mybigcommerce.com, set to Proxied (orange cloud). Cloudflare then routes traffic through your zone first, so your Worker runs before BigCommerce serves the page.
This only works for www and subdomains; an A record on the apex bypasses your Worker. If your store runs on the apex domain, redirect the apex to www and attach the Worker route to www.
Follow the Standard setup
Create the Worker, add the API key secret, and attach the route exactly as described in Standard setup above.
Verify the Worker is in the path
Run the checks in How to Test below. A crawler request to your store should come back with the x-lovablehtml-render-cache header. If it's missing, the DNS record is usually still an A record or not proxied, so traffic is skipping your zone.
Setup for Lovable, Base44, GHL and others
For domains connected to a Lovable, GHL AI Studio, Base44 (or similar) hosted project.
Create a new Worker
Open Cloudflare dashboard → choose a "Hello World" Worker → Deploy → Edit Code
Paste the snippet below into the Worker editor
Edit the two constants at the top: LOVABLE_UPSTREAM (your Lovable hosted URL, set above) and PUBLIC_HOST (your custom domain).
// encited.js (Cloudflare Worker - Custom Domain mode)// Use this when the Worker is attached as a Custom Domain in Cloudflare and// you need to forward non-prerendered traffic to your Lovable hosted URL.// CHANGE THIS: your Lovable hosted URL (e.g. https://yourapp.lovable.app)const LOVABLE_UPSTREAM = 'https://yourapp.lovable.app';// CHANGE THIS: your public custom domain (e.g. yourdomain.com)const PUBLIC_HOST = 'yourdomain.com';function isRedirect(status) {return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;}async function forwardToUpstream(req) {const upstreamBase = new URL(LOVABLE_UPSTREAM);const upstreamUrl = new URL(req.url);upstreamUrl.protocol = upstreamBase.protocol;upstreamUrl.hostname = upstreamBase.hostname;upstreamUrl.port = upstreamBase.port;const h = new Headers(req.headers);h.set('Host', upstreamBase.hostname);h.set('X-Forwarded-Host', PUBLIC_HOST);h.set('X-Forwarded-Proto', 'https');h.delete('cf-connecting-ip');h.delete('x-forwarded-for');h.delete('forwarded');const isGetLike = req.method === 'GET' || req.method === 'HEAD';const upstreamReq = new Request(upstreamUrl.toString(), {method: req.method,headers: h,body: isGetLike ? undefined : req.body,redirect: 'manual',});const resp = await fetch(upstreamReq);// Rewrite redirects so users stay on your custom domainif (isRedirect(resp.status)) {const loc = resp.headers.get('Location') || '';let newLoc = loc.replaceAll(upstreamBase.hostname, PUBLIC_HOST);newLoc = newLoc.replace(/^http:\/\//i, 'https://');const newHeaders = new Headers(resp.headers);if (loc) newHeaders.set('Location', newLoc);return new Response(resp.body, { status: resp.status, headers: newHeaders });}return resp;}export default {async fetch(req, env) {// Only handle public GET navigationsif (req.method !== 'GET') return forwardToUpstream(req);// Treat missing/empty Accept and bare '*/*' as HTML so crawler tests// (curl without -H, default fetch) still route through prerender.// Asset requests from browsers send specific Accept (e.g. 'text/css,*/*;q=0.1')// so they won't match.const accept = (req.headers.get('accept') || '').trim();const isHtmlRequest = !accept || accept === '*/*' || accept.includes('text/html');if (!isHtmlRequest) return forwardToUpstream(req);const headers = new Headers();headers.set('x-lovablehtml-api-key', env.LOVABLEHTML_API_KEY);headers.set('accept', 'text/html');const forward = ['accept-language','sec-fetch-mode','sec-fetch-site','sec-fetch-dest','sec-fetch-user','upgrade-insecure-requests','referer','user-agent',];for (const name of forward) {const v = req.headers.get(name);if (v) headers.set(name, v);}try {const r = await fetch('https://encited.com/api/prerender/render?url=' + encodeURIComponent(req.url),{ headers, redirect: 'manual' },);// 301 = configured redirect rule matched - forward to clientif (r.status === 301) {const loc = r.headers.get('location');if (loc) {return new Response(null, {status: 301,headers: { location: loc, 'cache-control': 'no-store' },});}}// 304 = not pre-rendered, fall through to upstreamif (r.status === 304) {return forwardToUpstream(req);}if (r.status === 200 && (r.headers.get('content-type') || '').includes('text/html')) {const responseHeaders = new Headers(r.headers);for (const name of ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'keep-alive']) {responseHeaders.delete(name);}responseHeaders.set('content-type', 'text/html; charset=utf-8');return new Response(r.body, { status: 200, headers: responseHeaders });}} catch {// Prerender unreachable → fall through so visitors still get the site}return forwardToUpstream(req);},};
Add the API key secret
Under Variables and Secrets, add a secret named LOVABLEHTML_API_KEY or run: wrangler secret put LOVABLEHTML_API_KEY
Deploy the Worker
Attach the Worker as a Custom Domain
Go to your Worker → Settings → Domains & Routes → Add → Custom domain → enter yourdomain.com. Add a second Custom Domain for www.yourdomain.com if you use www.
Wait for DNS and SSL
Cloudflare auto-creates the DNS record and SSL. You'll see a special Worker mapping in your DNS tab; leave it as is.
Verify the connection
Wait a couple of minutes for propagation. Hit your custom domain and confirm traffic reaches the Worker (and through it, Lovable).
Custom Domain, not Route
In this mode the Worker is the origin, so there is no proxied DNS record for it to sit behind. Use Custom domain attachment so Cloudflare manages DNS and SSL for you. Routes won't work without an upstream DNS record to intercept.
How to Test
- Call the render API directly and verify
x-lovablehtml-render-cache: hit | miss - Hit your site as a crawler and verify the response carries
x-lovablehtml-render-cache. That header is what proves the Worker served rendered HTML - Verify a normal browser request falls through to your existing origin
# 1) Call the Encited render API directlycurl -sS -D - -o /dev/null \-H "x-lovablehtml-api-key: <API_KEY>" \-H "Accept: text/html" \"https://encited.com/api/prerender/render?url=https%3A%2F%2Fyour-domain.com%2Fyour-page"# Look for:# - HTTP/1.1 200# - x-lovablehtml-render-cache: hit | miss# - x-lovablehtml-snapshot-key: ...
# 2) Hit your site with an HTML Accept headercurl -sS -D - -o /dev/null \-H "Accept: text/html" \-A "Googlebot" \"https://your-domain.com/your-page"# Look for:# - HTTP/1.1 200# - x-lovablehtml-render-cache: hit | miss## A plain 200 with content-type: text/html is what your origin returns# too. The x-lovablehtml-render-cache header proves the Worker served# rendered HTML.
# 3) Browser passthrough (Encited returns 304; the Worker serves your origin)curl -sS -D - -o /dev/null \-A "Mozilla/5.0" \"https://your-domain.com/your-page"# Look for:# - no x-lovablehtml-* headers (the response came from your origin)
Best Practices
- Keep secrets out of git: Store keys in your platform's secret manager or env vars; rotate/revoke when compromised.
- Don't proxy static assets: Only call Encited for HTML document requests. Always pass through JS/CSS/images/fonts.
- Handle 304 passthrough: 304 means prerendering doesn't apply; fall back to your origin. If you get a
301instead, a redirect rule matched; forward itsLocationheader to the client. - Invalidate after content changes: Use the cache invalidation endpoints (optionally with prewarm) after deploys or CMS updates.
Need help? Check the full API reference for prerender, cache, and analytics endpoint docs, or jump directly to Analytics API, or contact us if you run into issues.
