Encited logo
Pricing

How to Integrate Pre-rendering with Cloudflare Workers

Integrate pre-rendering with Cloudflare Workers so crawler requests receive rendered HTML while browser traffic continues to your origin.

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.

1

Create a new Worker

Open Cloudflare dashboard → choose a "Hello World" Worker → Deploy → Edit Code

2

Paste the snippet below into the Worker editor

encited.js
CopyDownload
// encited.js (Cloudflare Worker)
export default {
async fetch(req, env) {
// Only handle public GET navigations
if (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 client
if (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 origin
if (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);
},
};
3

Add the API key secret

Under Variables and Secrets, add a secret named LOVABLEHTML_API_KEY or run: wrangler secret put LOVABLEHTML_API_KEY

4

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.

5

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.

6

Set Failure mode

Set to Fail open (proceed) and save. If the Worker ever errors, requests continue straight to your origin instead of failing.

7

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.

8

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.

1

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.

2

Follow the Standard setup

Create the Worker, add the API key secret, and attach the route exactly as described in Standard setup above.

3

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.

If this feels too complicated, our no-code setup can handle it for you with a couple of DNS records.
Set project URL
1

Create a new Worker

Open Cloudflare dashboard → choose a "Hello World" Worker → Deploy → Edit Code

2

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
CopyDownload
// 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 domain
if (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 navigations
if (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 client
if (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 upstream
if (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);
},
};
3

Add the API key secret

Under Variables and Secrets, add a secret named LOVABLEHTML_API_KEY or run: wrangler secret put LOVABLEHTML_API_KEY

4

Deploy the Worker

5

Attach the Worker as a Custom Domain

Go to your Worker → Settings → Domains & Routes → AddCustom domain → enter yourdomain.com. Add a second Custom Domain for www.yourdomain.com if you use www.

6

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.

7

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

  1. Call the render API directly and verify x-lovablehtml-render-cache: hit | miss
  2. 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
  3. Verify a normal browser request falls through to your existing origin
bash
CopyDownload
# 1) Call the Encited render API directly
curl -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: ...
bash
CopyDownload
# 2) Hit your site with an HTML Accept header
curl -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.
bash
CopyDownload
# 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 301 instead, a redirect rule matched; forward its Location header 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.

Avatar
How can we help?
Get instant answers to your questions or leave a message for an engineer will reach out
Ask AI about Encited
See our docs
Contact support
Leave a message
We'll get back to you soon
Avatar
Ask AI about Encited
Team is also here to help
Thinking
Preview
Drop an image to attach
Powered by ReplyMaven