Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,46 @@
# Prerender.io – Lovable (Cloudflare Worker)

A Cloudflare Worker that proxies your Lovable app and routes bot traffic through Prerender.io for SEO rendering.
Cloudflare Workers that route bot traffic through Prerender.io for SEO rendering on Lovable apps.

## How it works
## Which worker to use

- Regular users are proxied transparently to your Lovable app (`LOVABLE_UPSTREAM`)
- Search engine bots and crawlers are routed to Prerender.io for server-side rendered HTML
- Canonical tags are injected/replaced to point to the public URL
- Redirects from the upstream are rewritten to use your public hostname
| File | For whom |
|------|----------|
| `worker-subscriber.js` | Lovable **paid subscribers** with a custom domain set up in Lovable's dashboard |
| `worker-custom-domain.js` | **Free Lovable users** who proxy their `yourapp.lovable.app` through Cloudflare to serve it from a custom domain |

## Setup
---

1. Deploy `worker.js` as a Cloudflare Worker
2. Set the following environment variables in your Worker settings:
## worker-subscriber.js (paid subscribers)

For Lovable paid plan users who configured their custom domain directly in Lovable. Your domain already points to Lovable's servers — this worker sits in front and routes crawler traffic to Prerender.io. No proxying needed.

### Setup

1. Deploy `worker-subscriber.js` as a Cloudflare Worker on your custom domain
2. Set the following environment variable:

| Variable | Description |
|----------|-------------|
| `PRERENDER_TOKEN` | Your Prerender.io token |

---

## worker-custom-domain.js (free users / custom proxy)

For free Lovable users who want to serve their app from a custom domain by proxying through Cloudflare. All traffic is forwarded to your Lovable app URL, with bots routed to Prerender.io and canonical tags injected.

### Setup

1. Deploy `worker-custom-domain.js` as a Cloudflare Worker on your custom domain
2. Set the following environment variables:

| Variable | Description |
|----------|-------------|
| `LOVABLE_UPSTREAM` | Your Lovable app URL (e.g. `https://yourapp.lovable.app`) |
| `PRERENDER_TOKEN` | Your Prerender.io token |

## Authentication / OAuth
### Authentication / OAuth

Lovable uses Supabase for authentication. When your app is served from a custom domain via this worker, the OAuth callback URL seen by the browser will be your custom domain (e.g. `https://example.com/auth/callback`), not the original Lovable URL.

Expand All @@ -36,4 +57,5 @@ Without this step, Supabase will reject the OAuth callback and users will not be

## Requirements

- Cloudflare Workers (supports `HTMLRewriter`)
- Cloudflare Workers
- `worker-custom-domain.js` additionally requires `HTMLRewriter` support (available on all Cloudflare Workers plans)
2 changes: 1 addition & 1 deletion worker.js → worker-custom-domain.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ async function handleRequest(request, env) {
const prerenderUrl = `https://service.prerender.io/${encodeURIComponent(url.href)}`;
const newHeaders = new Headers(request.headers);
newHeaders.set("X-Prerender-Token", env.PRERENDER_TOKEN);
newHeaders.set("X-Prerender-Int-Type", "CloudFlare-Lovable");
newHeaders.set("X-Prerender-Int-Type", "CloudFlare-Lovable-CustomDomain");
newHeaders.delete("Host");

const prerenderResp = await fetch(new Request(prerenderUrl, { headers: newHeaders, redirect: "manual" }));
Expand Down
72 changes: 72 additions & 0 deletions worker-subscriber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// User agents handled by Prerender
const BOT_AGENTS = [
// Search Engines
"googlebot", "adsbot-google", "apis-google", "mediapartners-google",
"google-safety", "feedfetcher-google", "googleproducer", "google-site-verification",
"bingbot", "yandexbot", "yabrowser", "yahoo", "baiduspider", "naver",
"seznambot", "sznprohlizec", "qwantbot", "ecosia", "duckduckbot",
"duckassistbot", "applebot",
// Social Media
"facebookexternalhit", "facebookcatalog", "facebookbot", "meta-externalagent",
"twitterbot", "linkedinbot", "whatsapp", "slackbot", "pinterest", "pinterestbot",
"tiktok", "tiktokspider", "bytespider", "discordbot",
// SEO Tools
"semrushbot", "ahrefsbot", "chrome-lighthouse", "screaming-frog",
"oncrawlbot", "botifybot", "deepcrawl", "lumar", "rogerbot", "dotbot",
// AI Bots
"gptbot", "chatgpt", "oai-searchbot", "chatgpt-user", "claudebot",
"google-extended", "perplexitybot", "perplexity-user", "youbot",
"amazonbot", "anthropic-ai", "claude-web", "claude-user", "ccbot", "mistralai-user",
// Other Known Bots & Crawlers
"embedly", "quora link preview", "showyoubot", "outbrain", "pinterest/0.",
"developers.google.com/+/web/snippet", "vkshare", "w3c_validator", "redditbot",
"flipboard", "tumblr", "bitlybot", "skypeuripreview", "nuzzel",
"google page speed", "qwantify", "bitrix link preview", "xing-contenttabreceiver",
"google-inspectiontool", "telegrambot",
// Testing
"integration-test",
];

const IGNORE_EXTENSIONS = [
".js", ".css", ".xml", ".less", ".png", ".jpg", ".jpeg", ".gif", ".pdf",
".doc", ".txt", ".ico", ".rss", ".zip", ".mp3", ".rar", ".exe", ".wmv",
".doc", ".avi", ".ppt", ".mpg", ".mpeg", ".tif", ".wav", ".mov", ".psd",
".ai", ".xls", ".mp4", ".m4a", ".swf", ".dat", ".dmg", ".iso", ".flv",
".m4v", ".torrent", ".woff", ".ttf", ".svg", ".webmanifest",
];

export default {
async fetch(request, env) {
return await handleRequest(request, env).catch(
(err) => new Response(err.stack, { status: 500 })
);
},
};

async function handleRequest(request, env) {
const url = new URL(request.url);
const userAgent = request.headers.get("User-Agent")?.toLowerCase() || "";
const isPrerender = request.headers.get("X-Prerender");
const pathName = url.pathname.toLowerCase();
const lastDot = pathName.lastIndexOf(".");
const extension = lastDot > -1 ? pathName.substring(lastDot).toLowerCase() : "";

if (
isPrerender ||
!BOT_AGENTS.some((bot) => userAgent.includes(bot.toLowerCase())) ||
(extension.length && IGNORE_EXTENSIONS.includes(extension))
) {
return fetch(request);
}

const newURL = `https://service.prerender.io/${request.url}`;
const newHeaders = new Headers(request.headers);

newHeaders.set("X-Prerender-Token", env.PRERENDER_TOKEN);
newHeaders.set("X-Prerender-Int-Type", "CloudFlare-Lovable-Proxy");

return fetch(new Request(newURL, {
headers: newHeaders,
redirect: "manual",
}));
}