Cloudflare Workers Integration
This guide will help you set up prerendering for your site via Cloudflare Workers. After setup, all bot requests will be automatically proxied through SeoRend.
Requirements
Section titled “Requirements”- Cloudflare account
- Domain connected to Cloudflare
- SeoRend account
Step-by-Step Guide
Section titled “Step-by-Step Guide”-
Create an API Key in SeoRend
Section titled “Create an API Key in SeoRend”Log into the SeoRend dashboard and go to the API Keys section.
- Click the Create API Key button
- Enter a key name (e.g.,
productionormy-site.com) - Click Create
- Copy the secret key — you’ll need it in the next step
The key looks something like:
sk_live_abc123def456... -
Add Secret to Cloudflare
Section titled “Add Secret to Cloudflare”Go to Cloudflare Dashboard and select your domain.
- In the left menu, select Workers & Pages
- Click Manage account-level secrets (or Settings → Variables)
- In the Environment Variables section, click Add variable
- Fill in the fields:
- Variable name:
SEOREND_API_KEY - Value: paste your API key from step 1
- Variable name:
- Check the Encrypt box (for security)
- Click Save
-
Create a Worker
Section titled “Create a Worker”- In Cloudflare Dashboard, go to Workers & Pages
- Click Create application → Create Worker
- Enter a Worker name (e.g.,
seorend-prerender) - Click Deploy (creates an empty Worker)
- After creation, click Edit code
-
Upload Worker Code
Section titled “Upload Worker Code”Replace the contents of
worker.jswith the SeoRend Worker code.Download the ready file:
Download cloudflare_worker.js
Or copy the code below. After pasting, click Save and Deploy.
// Seorend Cloudflare Worker// Detects bots and serves pre-rendered content, falls back to origin on errors
// Bot signatures — matched as lowercase substrings of User-Agentconst BOT_SIGNATURES = [ // Google "googlebot", "googleother", "storebot-google", "google-inspectiontool", "google-extended", "google-cloudvertexbot", "google-favicon", "apis-google", "adsbot-google", "mediapartners-google", "feedfetcher-google", "google page speed",
// Bing / Microsoft "bingbot", "bingpreview", "msnbot", "adidxbot", "microsoftpreview",
// Yandex "yandexbot", "yandeximages", "yandexvideo", "yandexmedia", "yandexmobilebot", "yandexmetrika", "yandexaccessibilitybot", "yabrowser",
// Baidu (all variants: image, video, news, mobile, render) "baiduspider",
// Other search engines "duckduckbot", "duckassistbot", "applebot", "naver", "sogouspider", "360spider", "coccoc", "seznambot", "qwantbot", "ecosia", "yahoo", "exabot", "petalbot", "bravebot",
// AI / LLM crawlers "gptbot", "oai-searchbot", "chatgpt-user", "chatgpt", "claudebot", "claude-web", "claude-user", "claude-searchbot", "anthropic-ai", "perplexitybot", "perplexity-user", "amazonbot", "ccbot", "meta-externalagent", "meta-externalfetcher", "mistralai-user", "cohere-ai", "ai2bot", "diffbot", "deepseekbot", "firecrawlagent", "kagi-fetcher", "bytespider", "tiktokspider", "xai-crawler", "timpibot", "anchor browser", "novellum ai crawl", "proratainc",
// Social media / messaging link previews "facebookexternalhit", "facebookcatalog", "facebookbot", "facebookplatform", "instagram", "twitterbot", "linkedinbot", "pinterestbot", "pinterest", "slackbot", "slack-imgproxy", "discordbot", "telegrambot", "whatsapp", "redditbot", "snapchat", "vkshare", "tumblr", "flipboard", "outbrain", "embedly", "bitlybot", "quora link preview", "line", "viber",
// SEO tools "semrushbot", "splitsignalbot", "ahrefsbot", "ahrefssiteaudit", "mj12bot", "rogerbot", "dotbot", "chrome-lighthouse", "screaming frog", "oncrawlbot", "botifybot", "deepcrawl", "lumar", "dataforseobot", "serpstatbot", "w3c_validator",
// Internet Archive "ia_archiver", "archive.org_bot",];
// Extensions that should never be prerenderedconst STATIC_EXTENSIONS = new Set([ ".js", ".css", ".xml", ".less", ".json", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif", ".pdf", ".doc", ".txt", ".rss", ".atom", ".zip", ".rar", ".7z", ".gz", ".tar", ".mp3", ".mp4", ".m4a", ".wav", ".mov", ".m4v", ".flv", ".webm", ".ogg", ".exe", ".dmg", ".iso", ".torrent", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".webmanifest", ".map", ".wasm",]);
// Paths that should never be prerenderedconst SKIP_PATHS = [ "/api/", "/graphql", "/_next/", "/__", "/ws", "/socket.io",];
function getFileExtension(pathname) { const p = (pathname || "").toLowerCase(); const lastSlash = p.lastIndexOf("/"); const file = lastSlash >= 0 ? p.slice(lastSlash + 1) : p; const dot = file.lastIndexOf("."); if (dot <= 0) return ""; return file.slice(dot);}
function isBotUserAgent(ua) { if (!ua) return false; const uaLower = ua.toLowerCase(); for (const sig of BOT_SIGNATURES) { if (uaLower.includes(sig)) return true; } return false;}
function shouldSkip(url) { // Skip static files const ext = getFileExtension(url.pathname); if (ext && STATIC_EXTENSIONS.has(ext)) return true;
// Skip API and internal paths const pathLower = url.pathname.toLowerCase(); for (const skip of SKIP_PATHS) { if (pathLower.startsWith(skip)) return true; }
return false;}
async function fetchFromOrigin(request) { return fetch(request);}
export default { async fetch(request, env, ctx) { const url = new URL(request.url); const ua = request.headers.get("user-agent") || "";
// Loop protection if (request.headers.get("x-seorend-processed") === "1") { return fetchFromOrigin(request); }
// Skip non-GET requests if (request.method !== "GET") { return fetchFromOrigin(request); }
// Skip static assets and API paths if (shouldSkip(url)) { return fetchFromOrigin(request); }
// Only bots get prerender if (!isBotUserAgent(ua)) { return fetchFromOrigin(request); }
// Config const RENDER_API_URL = "https://render.seorend.com/v1/render"; const apiKey = env.SEOREND_API_KEY;
if (!apiKey) { console.error("SEOREND_API_KEY not configured"); return fetchFromOrigin(request); }
try { // Call Seorend render API const renderResponse = await fetch(RENDER_API_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }, body: JSON.stringify({ url: request.url, userAgent: ua, }), });
// Handle specific error codes - fallback to origin if (renderResponse.status === 401 || renderResponse.status === 403) { // Auth error - misconfiguration, log and fallback console.error("Seorend auth error:", renderResponse.status); return fetchFromOrigin(request); }
if (renderResponse.status === 402 || renderResponse.status === 429) { // Payment required or rate limit - fallback to origin gracefully // Site continues to work, just without prerendering return fetchFromOrigin(request); }
if (renderResponse.status === 504) { // Render timeout - fallback to origin return fetchFromOrigin(request); }
if (!renderResponse.ok) { // Any other error - fallback to origin console.error("Seorend error:", renderResponse.status); return fetchFromOrigin(request); }
// Parse successful response const data = await renderResponse.json();
// Build response headers const headers = new Headers(); headers.set("Content-Type", "text/html; charset=utf-8"); headers.set("X-Seorend-Cache", data.cache?.hit ? "HIT" : "MISS"); headers.set("X-Seorend-Render-Time", String(data.timings?.total_ms || 0));
// Return pre-rendered HTML return new Response(data.html, { status: data.status || 200, headers, });
} catch (err) { // Network error or other exception - fallback to origin console.error("Seorend fetch error:", err.message || err); return fetchFromOrigin(request); } },};-
Connect Worker to Domain
Section titled “Connect Worker to Domain”- In Worker settings, go to the Triggers tab
- In the Routes section, click Add route
- Fill in the fields:
- Route:
example.com/*(replace with your domain) - Zone: select your domain
- Route:
- Click Save
-
Verify Proxied Mode
Section titled “Verify Proxied Mode”Make sure your domain’s DNS records go through Cloudflare:
- Go to DNS → Records
- Find the A or CNAME record for your domain
- Make sure the status shows an orange cloud (Proxied)
If the cloud is gray (DNS Only), click it to switch to Proxied mode.
Verification
Section titled “Verification”After setup, verify everything works correctly.
Test with curl
Section titled “Test with curl”Make a request with a bot User-Agent:
curl -A "Googlebot/2.1" -I https://example.com/If everything is set up correctly, you’ll see headers:
X-Seorend-Cache: HIT (or MISS on first request)Browser Test
Section titled “Browser Test”- Open DevTools (F12)
- Go to the Network tab
- Open your site
- Verify that the response doesn’t have
X-Seorend-*headers (regular users shouldn’t be proxied)
Troubleshooting
Section titled “Troubleshooting”Worker Not Triggering
Section titled “Worker Not Triggering”- Verify the Route is correct (with
/*at the end) - Make sure DNS is in Proxied mode
- Check Worker logs in the Logs section
401/403 Error
Section titled “401/403 Error”- Verify the
SEOREND_API_KEYsecret is created correctly - Make sure the API key is active in the SeoRend dashboard
429 Error
Section titled “429 Error”Render limit exceeded. Options:
- Wait for the new billing period
- Upgrade to a higher plan
- Enable Warm Bot to cache popular pages
Next Steps
Section titled “Next Steps”- Configure caching — optimize TTL for your content
- Enable Warm Bot — automatic warming of popular pages