diff --git a/README.md b/README.md index 6ad65b1..fcb4146 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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) diff --git a/worker.js b/worker-custom-domain.js similarity index 98% rename from worker.js rename to worker-custom-domain.js index b9ed5f7..93b028f 100644 --- a/worker.js +++ b/worker-custom-domain.js @@ -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" })); diff --git a/worker-subscriber.js b/worker-subscriber.js new file mode 100644 index 0000000..c50edd9 --- /dev/null +++ b/worker-subscriber.js @@ -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", + })); +}